Ответ 1
Чтобы найти стандартные библиотечные функции, Hoogle - отличный ресурс; это поисковая система Haskell, которая позволяет вам искать по типу. Использование этого требует выяснения того, как думать о типах Haskell Way ™, хотя, с которыми ваши предлагаемые типы подписей не совсем работают. Итак:
-
Вы ищете
[Filepath] -> [Filepath]
. Помните, что написание HaskellFilePath
. Так что... -
Вы ищете
[Filepath] -> [Filepath]
. Это не нужно; если вы хотите фильтровать вещи, вы должны использоватьfilter
. Так что... -
Вы ищете функцию типа
FilePath -> Bool
, которую вы можете передать вfilter
. Но это не совсем правильно: эта функция должна запрашивать файловую систему, которая является эффектом, а эффекты Haskell отслеживают в системе типов с помощьюIO
. Так что... -
Вы ищете функцию типа
FilePath -> IO Bool
.
И если мы ищем это в Hoogle, первый результат doesFileExist :: FilePath -> IO Bool
из System.Directory
. Из документов:
Операция
doesFileExist
возвращаетTrue
если файл аргумента существует и не является каталогом, аFalse
в противном случае.
Итак, System.Directory.doesFileExist
- это именно то, что вы хотите. (Ну... только с небольшой дополнительной работой! См. Ниже.)
Теперь, как вы его используете? Вы не можете использовать filter
здесь, потому что у вас есть эффективная функция. Вы можете снова использовать Hoogle - если filter
имеет тип (a -> Bool) -> [a] -> [a]
, тогда аннотирование результатов функций с помощью монады 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
, чтобы их можно было интерпретировать правильно. (Мы также отбрасываем результаты .
и ..
, хотя, если вы просто ищете обычные файлы, они ничего не повредят.) Это вернет имена файлов, которые включают каталог, содержимое которого проверяется. Функция настройки getDirectoryContents
выглядит следующим образом:
getQualifiedDirectoryContents :: FilePath -> IO [FilePath]
getQualifiedDirectoryContents fp =
map (fp </>) . filter (`notElem` [".",".."]) <$> getDirectoryContents fp
filter
избавляется от специальных каталогов, а map
добавляет каталог аргументов ко всем результатам. Это делает возвращаемые файлы приемлемыми аргументами для doesFileExist
. (Если вы их раньше не видели, (System.FilePath.</>)
добавляет два пути к файлу и (Control.Applicative.<$>)
, также доступный как (Data.Functor.<$>)
, является синонимом infix для 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 "."
Это то, что мы хотим, но, к сожалению, оно доступно только в версии 1.3.2.0 пакета directory
- на момент написания этой статьи, самой последней, а не той, которая у меня есть. К счастью, это простая функция для реализации; такие функции set-a-value-local обычно реализуются в терминах 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 "."
Кроме того, одно быстрое примечание о let
в do
s: в блоке do
,
do ...foo...
let x = ...bar...
...baz...
эквивалентно
do ...foo...
let x = ...bar... in
do ...baz...
Таким образом, ваш примерный код не нужен in
в let
и может вызывать вызов print
.
¹ Не всегда: иногда вам нужны разные классы эффектов! Используйте Applicative
из Control.Applicative
, когда это возможно; больше вещей Applicative
, чем Monad
(хотя это означает, что вы можете сделать меньше с ними). В этом случае эффективные функции могут жить там, а также в Data.Foldable
или Data.Traversable
.