要查找标准库函数,Hoogle 是一个很好的资源;它是一个 Haskell 搜索引擎,可让您按类型搜索。但是,使用它需要弄清楚如何考虑 Haskell Way™ 的类型,而您提出的类型签名并不完全适用。所以:
您正在寻找[Filepath] -> [Filepath]。请记住,Haskell 的拼写是FilePath。所以……
您正在寻找[FilePath] -> [FilePath]。这是不必要的;如果你想过滤东西,你应该使用filter。所以……
您正在寻找可以传递给filter 的FilePath -> Bool 类型的函数。但这不太对:这个函数需要查询文件系统,这是一个效果,Haskell 使用IO 跟踪类型系统中的效果。所以……
您正在寻找FilePath -> IO Bool 类型的函数。
而if we search for that on Hoogle,第一个结果是doesFileExist :: FilePath -> IO Bool from System.Directory。来自文档:
如果参数文件存在且不是目录,则操作doesFileExist返回True,否则返回False。
所以System.Directory.doesFileExist 正是您想要的。 (嗯……只需要一点额外的工作!见下文。)
现在,你如何使用它?你不能在这里使用filter,因为你有一个有效的功能。你可以再次使用 Hoogle——如果 filter 的类型为 (a -> Bool) -> [a] -> [a],那么使用 monad m 注释函数的结果会给你新的类型 Monad m => (a -> m Bool) -> [a] -> m [Bool]——但有一个更简单的“廉价技巧”。一般来说,如果func 是一个具有有效/单子版本的函数,则该有效/单子版本称为funcM,它通常存在于Control.Monad。¹事实上,有一个函数Control.Monad.filterM :: Monad m => (a -> m Bool) -> [a] -> m [a]。
但是! 尽管我们不愿意承认,即使在 Haskell 中,类型也不能提供您需要的所有信息。重要的是,我们会在这里遇到问题:
因此,我们可以采取两种方法来解决问题。首先是调整getDirectoryContents 的结果,以便能够正确解释它们。 (我们还丢弃了. 和.. 结果,尽管如果您只是在寻找常规文件,它们不会有任何损害。)这将返回包含正在检查其内容的目录的文件名。调整getDirectoryContents 函数如下所示:
getQualifiedDirectoryContents :: FilePath -> IO [FilePath]
getQualifiedDirectoryContents fp =
map (fp </>) . filter (`notElem` [".",".."]) <$> getDirectoryContents fp
filter 去掉了特殊目录,map 将参数目录添加到所有结果中。这使得返回的文件可以接受 doesFileExist 的参数。 (如果您以前没有见过它们,(System.FilePath.</>) 会附加两个文件路径;而(Control.Applicative.<$>),也可用作(Data.Functor.<$>),是fmap 的中缀同义词,类似于liftM,但适用范围更广。 )
将所有这些放在一起,您的最终代码变为:
import Control.Applicative
import Control.Monad
import System.FilePath
import System.Directory
getQualifiedDirectoryContents :: FilePath -> IO [FilePath]
getQualifiedDirectoryContents fp =
map (fp </>) . filter (`notElem` [".",".."]) <$> getDirectoryContents fp
main :: IO ()
main = do
contents <- getQualifiedDirectoryContents "/foo/bar"
onlyFiles <- filterM doesFileExist contents
print onlyFiles
或者,如果您喜欢花哨/无意义:
import Control.Applicative
import Control.Monad
import System.FilePath
import System.Directory
getQualifiedDirectoryContents :: FilePath -> IO [FilePath]
getQualifiedDirectoryContents fp =
map (fp </>) . filter (`notElem` [".",".."]) <$> getDirectoryContents fp
main :: IO ()
main = print
=<< filterM doesFileExist
=<< getQualifiedDirectoryContents "/foo/bar"
第二种方法是进行调整,使doesFileExist 在适当的当前目录下运行。这将只返回相对于正在检查其内容的目录的文件名。为此,我们要使用withCurrentDirectory :: FilePath -> IO a -> IO a 函数(但见下文),然后将getDirectoryContents 传递给当前目录"." 参数。 withCurrentDirectory 的文档(部分)说:
使用给定的工作目录运行IO 操作并在之后恢复原始工作目录,即使给定的操作由于异常而失败。
把所有这些放在一起,我们得到了以下代码
import Control.Monad
import System.Directory
main :: IO ()
main = withCurrentDirectory "/foo/bar" $
print =<< filterM doesFileExist =<< getDirectoryContents "."
这正是我们想要的,但不幸的是,它仅在 directory 软件包的 1.3.2.0 版本中可用——在撰写本文时,它是最新的,而不是我拥有的。幸运的是,这是一个易于实现的功能。此类本地设置值功能通常以Control.Exception.bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c 的形式实现。 bracket 函数以bracket before after action 运行,它可以正确处理异常。所以我们可以自己定义withCurrentDirectory:
withCurrentDirectory :: FilePath -> IO a -> IO a
withCurrentDirectory fp m =
bracket getCurrentDirectory setCurrentDirectory $ \_ -> do
setCurrentDirectory fp
m
然后用这个得到最终的代码:
import Control.Exception
import Control.Monad
import System.Directory
withCurrentDirectory :: FilePath -> IO a -> IO a
withCurrentDirectory fp m =
bracket getCurrentDirectory setCurrentDirectory $ \_ -> do
setCurrentDirectory fp
m
main :: IO ()
main = withCurrentDirectory "/foo/bar" $
print =<< filterM doesFileExist =<< getDirectoryContents "."
另外,关于dos 中的lets 的简短说明:在do 块中,
do ...foo...
let x = ...bar...
...baz...
等价于
do ...foo...
let x = ...bar... in
do ...baz...
因此,您的示例代码不需要let 中的in,并且可以超出print 调用。
¹ 并非总是如此:有时您需要不同类别的效果!尽可能使用Control.Applicative 中的Applicative; Applicatives 比 Monads 更多的东西(尽管这意味着你可以用它们做更少的事情)。在这种情况下,有效的函数可能存在于那里,或者也存在于Data.Foldable 或Data.Traversable。