контакт с IO по сравнению с чистым кодом в haskell

Я пишу сценарий оболочки (мой 1-й непример в 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
10
задан Drakosha 30 May 2010 в 05:31
поделиться

2 ответа

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

  1. do a a
  2. do а; б; c ... a >> делать б; c ...
  3. do x <- a; б; 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

Далее, как указал Кенни, у вас есть 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 -> mb) -> [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 , вы можете написать это , сравнивая 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 , что сделало бы два эквивалентными по длине. * (Я мог бы даже встроить fileSize , если бы он не использовался где-то еще.) Во-вторых,Мне больше нравится эта версия, потому что она включает в себя объединение более простых чистых функций, которые мы уже написали. Хотя ваша версия похожа, моя (я чувствую) более обтекаемая и проясняет этот аспект.

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

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

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

13
ответ дан 3 December 2019 в 17:19
поделиться

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 требует, чтобы функция ввода возвращала материал ввода-вывода :

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)
10
ответ дан 3 December 2019 в 17:19
поделиться
Другие вопросы по тегам:

Похожие вопросы: