代码之家  ›  专栏  ›  技术社区  ›  Drakosha

处理Haskell中的IO与纯代码

  •  10
  • Drakosha  · 技术社区  · 14 年前

    我正在编写一个shell脚本(我在haskell中的第一个非示例),它应该列出一个目录,获取每个文件的大小,执行一些字符串操作(纯代码),然后重命名一些文件。我不知道我做错了什么,所以有两个问题:

    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 回复  |  直到 11 年前
        1
  •  13
  •   Antal Spector-Zabusky    11 年前

    您的第二个问题是您的函数类型。但是,您的第一个问题(不是真正的类型)是 do 语句在 getFileNameAndSize . 同时 与monads一起使用,它不是一种单一的灵丹妙药;它实际上实现为 some simple translation rules . 克里夫的笔记版本(不是 确切地 是的,由于一些涉及错误处理的细节,但是非常接近)是:

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

    换言之, 获取文件名和大小 相当于没有 阻止,这样你就可以摆脱 . 这就留给你

    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 ... )这可能只是一个简单的疏忽。

    下一步,我们来到你错误的核心: files <- (mapM getFileNameAndSize fileNames) . 我不知道它为什么会给你准确的错误,但我可以告诉你出了什么问题。记住我们所知道的 获取文件名和大小 . 在代码中,它返回 (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 Monad中的结果,而不是简单的列表;函数 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 . 其他的一切都是一个普通的纯函数(只有当它是单态的时候,因为一般的原因,沿着 保藏 )然后我安排事情 主要的 等等,只是纯函数的组成和链: 主要的 从中获取一些值 输入输出 -着陆;然后它调用纯函数来折叠、旋转和破坏日期;然后它得到更多 输入输出 值;然后它操作更多;等等。其思想是尽可能地将两个域分离开来,从而使更多的复合非- 输入输出 代码总是免费的,黑匣子 输入输出 只有在必要的时候才能做到。

    类算子 <= & lt; 确实有助于以这种方式编写代码,因为它们允许您操作 功能 与一元值(如 输入输出 -世界)就像你在正常功能上操作一样。你也应该看看 Control.Applicative's function <$> liftedArg1 <*> liftedArg2 <*> ... 符号,它允许您将普通函数应用于任意数量的一元(实际上 Applicative )争论。这真的是很好的摆脱虚假 <- 在一元代码上链接纯函数。

    *:我觉得 保藏 或者至少是它的兄弟姐妹 preserving :: (a -> b) -> a -> (a,b) 应该在某个包裹里,但我也找不到。

        2
  •  10
  •   kennytm    14 年前

    getDirectoryContents is a function . 你应该为它提供一个论据,例如

    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 requires the input function to return an 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)