Работа с IO против чистого кода в haskell

Я пишу оболочку script (мой первый не-пример в haskell), который должен перечислить каталог, получить каждый размер файла, выполнить некоторые манипуляции с строкой (чистый код), а затем переименовать некоторые файлы. Я не уверен, что я делаю неправильно, поэтому 2 вопроса:

  • Как мне организовать код в такой программе?
  • У меня есть конкретная проблема, я получаю следующую ошибку: что я делаю неправильно?
error:
    Couldn't match expected type `[FilePath]'
           against inferred type `IO [FilePath]'
    In the second argument of `mapM', namely `fileNames'
    In a stmt of a 'do' expression:
        files <- (mapM getFileNameAndSize fileNames)
    In the expression:
        do { fileNames <- getDirectoryContents;
             files <- (mapM getFileNameAndSize fileNames);
             sortBy cmpFilesBySize files }

код:

getFileNameAndSize fname = do (fname,  (withFile fname ReadMode hFileSize))

getFilesWithSizes = do
  fileNames <- getDirectoryContents
  files <- (mapM getFileNameAndSize fileNames)
  sortBy cmpFilesBySize files

Ответ 1

Вторая, конкретная проблема связана с типами ваших функций. Тем не менее, ваша первая проблема (не совсем вещь типа) - это оператор do в getFileNameAndSize. Хотя do используется с монадами, это не монадическая панацея; он фактически реализован как несколько простых правил трансляции. Версия Cliff Notes (что не совсем верно, благодаря некоторым деталям, связанным с обработкой ошибок, но достаточно близко):

  • do aa
  • do a ; b ; c ...a >> do b ; c ...
  • do x <- a ; b ; c ...a >>= \x -> do b ; c ...

Другими словами, getFileNameAndSize эквивалентен версии без блока do, и поэтому вы можете избавиться от do. Это оставляет вас с

getFileNameAndSize fname = (fname, withFile fname ReadMode hFileSize)

Мы можем найти тип для этого: поскольку fname является первым аргументом withFile, он имеет тип FilePath; и hFileSize возвращает a IO Integer, поэтому тип withFile .... Таким образом, имеем getFileNameAndSize :: FilePath -> (FilePath, IO Integer). Это может быть или не быть тем, чего вы хотите; вы можете вместо этого хотеть FilePath -> IO (FilePath,Integer). Чтобы изменить его, вы можете написать любой из

getFileNameAndSize_do    fname = do size <- withFile fname ReadMode hFileSize
                                    return (fname, size)
getFileNameAndSize_fmap  fname = fmap ((,) fname) $
                                      withFile fname ReadMode hFileSize
-- With `import Control.Applicative ((<$>))`, which is a synonym for fmap.
getFileNameAndSize_fmap2 fname =     ((,) fname)
                                 <$> withFile fname ReadMode hFileSize
-- With {-# LANGUAGE TupleSections #-} at the top of the file
getFileNameAndSize_ts    fname = (fname,) <$> withFile fname ReadMode hFileSize

Далее, как заметил KennyTM, у вас есть fileNames <- getDirectoryContents; поскольку getDirectoryContents имеет тип FilePath -> IO FilePath, вам нужно дать ему аргумент. (например, getFilesWithSizes dir = do fileNames <- getDirectoryContents dir ...). Это, вероятно, просто промах.

Mext, мы подошли к вашей ошибке: files <- (mapM getFileNameAndSize fileNames). Я не знаю, почему это дает вам точную ошибку, но я могу сказать вам, что случилось. Помните, что мы знаем о getFileNameAndSize. В вашем коде он возвращает (FilePath, IO Integer). Однако mapM имеет тип Monad m => (a -> m b) -> [a] -> m [b], поэтому mapM getFileNameAndSize не типизирован. Вы хотите getFileNameAndSize :: FilePath -> IO (FilePath,Integer), как я уже сделал выше.

Наконец, нам нужно исправить вашу последнюю строку. Прежде всего, хотя мы не даем его нам, cmpFilesBySize, по-видимому, является функцией типа (FilePath, Integer) -> (FilePath, Integer) -> Ordering, сравнивая со вторым элементом. Это действительно просто: используя Data.Ord.comparing :: Ord a => (b -> a) -> b -> b -> Ordering, вы можете написать этот comparing snd, который имеет тип Ord b => (a, b) -> (a, b) -> Ordering. Во-вторых, вам нужно вернуть результат, завершенный в монаде IO, а не просто как простой список; функция return :: Monad m => a -> m a выполнит трюк.

Таким образом, положив это все вместе, вы получите

import System.IO           (FilePath, withFile, IOMode(ReadMode), hFileSize)
import System.Directory    (getDirectoryContents)
import Control.Applicative ((<$>))
import Data.List           (sortBy)
import Data.Ord            (comparing)

getFileNameAndSize :: FilePath -> IO (FilePath, Integer)
getFileNameAndSize fname = ((,) fname) <$> withFile fname ReadMode hFileSize

getFilesWithSizes :: FilePath -> IO [(FilePath,Integer)]
getFilesWithSizes dir = do fileNames <- getDirectoryContents dir
                           files     <- mapM getFileNameAndSize fileNames
                           return $ sortBy (comparing snd) files

Это хорошо, хорошо, и все будет хорошо. Однако я мог бы написать это несколько иначе. Моя версия, вероятно, будет выглядеть так:

{-# LANGUAGE TupleSections #-}
import System.IO           (FilePath, withFile, IOMode(ReadMode), hFileSize)
import System.Directory    (getDirectoryContents)
import Control.Applicative ((<$>))
import Control.Monad       ((<=<))
import Data.List           (sortBy)
import Data.Ord            (comparing)

preservingF :: Functor f => (a -> f b) -> a -> f (a,b)
preservingF f x = (x,) <$> f x
-- Or liftM2 (<$>) (,), but I am not entirely sure why.

fileSize :: FilePath -> IO Integer
fileSize fname = withFile fname ReadMode hFileSize

getFilesWithSizes :: FilePath -> IO [(FilePath,Integer)]
getFilesWithSizes = return .   sortBy (comparing snd)
                           <=< mapM (preservingF fileSize)
                           <=< getDirectoryContents 

(<=< является монадическим эквивалентом ., оператора композиции функций.) Прежде всего: да, моя версия дольше. Тем не менее, у меня, вероятно, уже есть preservingF где-то определен, что делает два эквивалентных по длине. * (Я мог бы даже встроить fileSize, если бы он не использовался в другом месте.) Во-вторых, мне нравится эта версия лучше, потому что она включает в себя цепочку вместе простые простые функции, которые мы уже писали. Хотя ваша версия похожа, моя (я чувствую) более оптимизирована и делает этот аспект вещей более ясным.

Итак, это немного ответ на ваш первый вопрос о том, как структурировать эти вещи. Я лично склонен блокировать мое IO до нескольких функций, как только возможные функции, которые нужно напрямую касаться внешнего мира (например, main и все, что взаимодействует с файлом) получают IO. Все остальное - обычная чистая функция (и только монадическая, если она монадична по общим причинам, по строкам preservingF). Затем я упорядочиваю вещи так, что main и т.д. - это просто композиции и цепочки чистых функций: main получает некоторые значения из IO -land; то он называет чистые функции, чтобы сбрасывать, шпинделя и калечить дату; то он получает больше значений IO; то он работает больше; и т.д. Идея состоит в том, чтобы как можно больше отделить эти домены, так что более композитный код <<246 > всегда свободен, а черный ящик IO выполняется только там, где это необходимо.

Операторы, такие как <=<, действительно помогают писать код в этом стиле, поскольку они позволяют вам работать с функциями, которые взаимодействуют с монадическими значениями (такими как IO -world) так же, как вы будете работать с обычными функциями. Вы также должны посмотреть на примечание Control.Applicative function <$> liftedArg1 <*> liftedArg2 <*> ..., которое позволяет применять обычные функции к любому числу монадических (действительно Applicative)) аргументы. Это действительно хорошо для избавления от ложных <- и просто цепочки чистых функций над монадическим кодом.

*: Мне кажется, что preservingF или, по крайней мере, его брат preserving :: (a -> b) -> a -> (a,b), должен быть где-то в пакете, но я тоже не смог найти.

Ответ 2

getDirectoryContents - это функция. Вы должны указать аргумент, например.

fileNames <- getDirectoryContents "/usr/bin"

Кроме того, тип getFileNameAndSize равен FilePath -> (FilePath, IO Integer), как вы можете проверить из ghci:

Prelude> :m + System.IO
Prelude System.IO> let getFileNameAndSize fname = do (fname, (withFile fname ReadMode hFileSize))
Prelude System.IO> :t getFileNameAndSize
getFileNameAndSize :: FilePath -> (FilePath, IO Integer)

Но mapM требует, чтобы функция ввода возвращала IO stuff:

Prelude System.IO> :t mapM
mapM :: (Monad m) => (a -> m b) -> [a] -> m [b]
-- #                  ^^^^^^^^

Вы должны изменить его тип на FilePath -> IO (FilePath, Integer), чтобы он соответствовал типу.

getFileNameAndSize fname = do
  fsize <- withFile fname ReadMode hFileSize
  return (fname, fsize)