2010-05-29 36 views
10

我正在编写一个shell脚本(我在haskell中的第一个非示例),它应该列出一个目录,获取每个文件大小,执行一些字符串操作(纯代码),然后重命名一些文件。我不知道我在做什么错,所以2个问题:处理IO vs 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 

回答

13

你的第二个问题是你的函数的类型。但是,您的第一个问题(不是真正的类型)是getFileNameAndSize中的do声明。虽然do与单子一起使用,但它并不是单一的万能药;它实际上实施为some simple translation rules。悬崖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。这使你与

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。其次,你需要返回你在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。其他一切都是一个普通的纯函数(如果它是一般性原因的单调性,则只有一次性,如preservingF)。然后,我安排一些东西,以使main等仅仅是纯函数的组合和链:mainIO -land获得一些值;那么它就称纯粹的功能来折叠,旋转和破坏日期;那么它会得到更多的IO值;那么它运作得更多;等等。这个想法是尽可能地分开两个域,以便更多的组合非代码总是空闲的,并且黑盒子IO只在必要时才精确完成。

运营商像<=<真的写代码有助于这种风格,因为他们让你在功能与一元值(如IO -world)进行交互,就像你会在正常功能运行操作。你还应该看Control.Applicative'sfunction <$> liftedArg1 <*> liftedArg2 <*> ...表示法,它允许你将普通函数应用于任意数量的monadic(真正的Applicative)参数。这对于摆脱伪造的<- s以及仅仅通过一元代码链接纯函数是非常好的。

*:我觉得像preservingF,或者至少它的兄弟preserving :: (a -> b) -> a -> (a,b),应该放在某个地方,但我一直都找不到。

+0

谢谢,很好的答案,我仍然理解很少,我猜,因为我理解它的40%,但它解决了这个问题;) – Drakosha 2010-05-30 05:30:11

+0

很高兴我能帮上忙。有什么特别的你没有得到?接近尾声的东西更多的是我认为你可能想要看/学习的东西,而不是我认为你应该已经知道的东西。 – 2010-05-31 20:57:54

10

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) 
+1

谢谢,很好的答案 – Drakosha 2010-05-30 05:29:19