【问题标题】:dealing with IO vs pure code in haskell在 haskell 中处理 IO 与纯代码
【发布时间】:2010-05-29 07:50:19
【问题描述】:

我正在编写一个 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

【问题讨论】:

    标签: haskell io


    【解决方案1】:

    您的第二个具体问题是您的函数类型。但是,您的第一个问题(不是真正的类型)是getFileNameAndSize 中的do 语句。虽然do 与单子一起使用,但它不是单子的灵丹妙药;它实际上实现为some simple translation rules。 Cliff's Notes 版本(完全正确,这要归功于涉及错误处理的一些细节,但足够接近)是:

    1. do a ≡ a
    2. do a ; b ; c ... ≡ a &gt;&gt; do b ; c ...
    3. do x &lt;- a ; b ; c ... ≡ a &gt;&gt;= \x -&gt; do b ; c ...

    换句话说,getFileNameAndSize 相当于没有do 块的版本,所以你可以去掉do。这给你留下了

    getFileNameAndSize fname = (fname, withFile fname ReadMode hFileSize)
    

    我们可以找到它的类型:因为fname 是withFile 的第一个参数,所以它的类型是FilePath;而hFileSize 返回一个IO Integer,这就是withFile ... 的类型。因此,我们有getFileNameAndSize :: FilePath -&gt; (FilePath, IO Integer)。这可能是也可能不是您想要的;你可能想要FilePath -&gt; 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 &lt;- getDirectoryContents;因为getDirectoryContents 的类型为FilePath -&gt; IO FilePath,所以你需要给它一个参数。 (例如getFilesWithSizes dir = do fileNames &lt;- getDirectoryContents dir ...)。这可能只是一个简单的疏忽。

    Mext,我们找到了您错误的核心:files &lt;- (mapM getFileNameAndSize fileNames)。我不确定为什么它会给你它的精确错误,但我可以告诉你哪里出了问题。记住我们对getFileNameAndSize 的了解。在您的代码中,它返回一个(FilePath, IO Integer)。但是,mapM 的类型为 Monad m =&gt; (a -&gt; m b) -&gt; [a] -&gt; m [b],因此 mapM getFileNameAndSize 的类型错误。你想要getFileNameAndSize :: FilePath -&gt; IO (FilePath,Integer),就像我在上面实现的那样。

    最后,我们需要修复您的最后一行。首先,虽然你没有给我们,cmpFilesBySize 大概是(FilePath, Integer) -&gt; (FilePath, Integer) -&gt; Ordering 类型的函数,比较第二个元素。不过,这真的很简单:使用Data.Ord.comparing :: Ord a =&gt; (b -&gt; a) -&gt; b -&gt; b -&gt; Ordering,您可以编写comparing snd,它的类型为Ord b =&gt; (a, b) -&gt; (a, b) -&gt; Ordering。其次,您需要返回包含在 IO monad 中的结果,而不仅仅是一个简单的列表;函数return :: Monad m =&gt; a -&gt; 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 
    

    (&lt;=&lt; 是函数组合运算符. 的一元等效项。)首先:是的,我的版本更长。但是,我可能已经在某处定义了preservingF,使两者的长度相等。*(如果它没有在其他地方使用,我什至可能内联fileSize。)其次,我更喜欢这个版本,因为它涉及链接我们已经编写了更简单的纯函数。虽然您的版本相似,但我的(我觉得)更精简,使这方面的事情更清晰。

    所以这是对如何构建这些东西的第一个问题的回答。我个人倾向于将我的 IO 锁定在尽可能少的函数中——只有需要直接接触外部世界的函数(例如main 和任何与文件交互的函数)才会获得IO .其他一切都是一个普通的纯函数(并且只有在出于一般原因它是一元的时才是一元的,沿着preservingF 的行)。然后我安排事情,使main 等只是纯函数的组合和链:main 从IO-land 获得一些值;然后它调用纯函数来折叠、旋转和破坏日期;然后它会得到更多的IO 值;然后它运行得更多;其想法是尽可能将两个域分开,以便组合性更强的非IO 代码始终是免费的,而黑盒IO 仅在必要时精确完成。

    像&lt;=&lt; 这样的操作符确实有助于以这种风格编写代码,因为它们让您可以像操作一样操作与一元值交互的函数(例如IO-world)在正常功能上运行。您还应该查看Control.Applicative'sfunction &lt;$&gt; liftedArg1 &lt;*&gt; liftedArg2 &lt;*&gt; ... 表示法,它允许您将普通函数应用于任意数量的单子(实际上是Applicative)参数。这对于摆脱虚假的&lt;-s 并在单子代码上链接纯函数非常有用。

    *:我觉得preservingF,或者至少是它的兄弟preserving :: (a -&gt; b) -&gt; a -&gt; (a,b),应该在某个包中,但我一直找不到。

    【讨论】:

    • 谢谢,很好的答案,我猜我仍然理解的很少,因为我理解了其中的 40%,但它解决了问题 ;)
    • 很高兴我能帮上忙。有什么特别你没有得到的吗?最后的很多东西是我认为你可能想看看/学习的东西,而不是我认为你应该已经知道的东西。
    【解决方案2】:

    getDirectoryContents is a function。您应该为其提供一个参数,例如

    fileNames <- getDirectoryContents "/usr/bin"
    

    另外,getFileNameAndSize 的类型是FilePath -&gt; (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 -&gt; IO (FilePath, Integer) 以匹配类型。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-03-20
      • 1970-01-01
      • 2013-05-01
      • 1970-01-01
      • 1970-01-01
      • 2015-12-10
      • 1970-01-01
      • 2014-03-30
      相关资源
      最近更新 更多