работа с IO против чистого кода в haskell - PullRequest
10 голосов
/ 29 мая 2010

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

  1. Как мне разместить код в такой программе?
  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

Ответы [ 2 ]

13 голосов
/ 29 мая 2010

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

  1. do aa
  2. do a ; b ; c ...a >> do b ; c ...
  3. do x <- a ; b ; c ...a >>= \x -> do b ; c ...

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

getFileNameAndSize fname = (fname, withFile fname ReadMode hFileSize)

Мы можем найти тип для этого: поскольку fname является первым аргументом withFile, он имеет тип FilePath; и hFileSize возвращает 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. Во-вторых, вам нужно возвращать свой результат, заключенный в монаду ввода-вывода, а не просто как простой список; функция 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, что сделало бы два эквивалентных по длине *. вместе более простые чистые функции мы уже написали. Хотя ваша версия похожа, моя (я чувствую) более упорядочена и проясняет этот аспект.

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

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

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

10 голосов
/ 29 мая 2010

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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...