【问题标题】:Iterate inside a do block在 do 块内迭代
【发布时间】:2012-09-11 19:30:49
【问题描述】:

我写了这段代码:

toCouplesFile = do inputFile <- openFile "deletedId.csv" ReadMode
                   outputFile <- openFile "couples.txt" WriteMode
                   readAndChange inputFile outputFile

readAndChange i o = do iseof <- hIsEOF i
                       if iseof then (return o)
                       else do line <- hGetLine i
                               hPutStrLn o (show (extractNameAndId line))
                               readAndChange i o

我想知道我是否可以只使用一个函数重写这段代码,使用类似于这种模式的东西:

function x = do ...
                label
                .....
                if ... then label else exit

【问题讨论】:

    标签: haskell monads


    【解决方案1】:

    以不必要的命令式编程方式让生活变得困难。您正在使用优美的 Haskell 语言进行编程,并且正在寻找 goto 构造!

    为什么不只是import Control.Applicative (&lt;$&gt;) 和写

    readAndChange' = writeFile "couples.txt" =<< 
        unlines.map (show.extractNameAndId).lines <$> readFile "deletedId.csv" 
    

    (是的,这几乎是单行代码。它采用简洁、实用的风格,并且没有被读写行的机制所打乱。尽可能多地用纯代码完成处理,只有输入和输出是 IO-基于。)

    说明:

    在这里,unlines.map (show.extractNameAndId).lines 处理您的输入,将其分成几行,然后使用mapextractNameAndId 应用到每个输入,然后使用unlines 将它们重新连接在一起。

    unlines.map (show.extractNameAndId).lines &lt;$&gt; readFile "deletedId.csv" 将读取文件并应用处理函数。 &lt;$&gt;fmap 的令人愉悦的语法。

    writeFile "couples.txt" =&lt;&lt; getanswergetanswer &gt;&gt;= writeFile "couples.txt" 相同 - 得到上面的答案然后将其写入文件。

    尝试写 greet xs = "hello " ++ xs 然后在 ghci 中做这些以获得乐趣

    greet "Jane"        -- apply your pure function purely
    greet $ "Jane"      -- apply it purely again
    greet <$> ["Jane","Craig","Brian"]  -- apply your function on something that produces three names
    greet <$> Just "Jane"               -- apply your function on something that might have a name
    greet <$> Nothing                   -- apply your function on something that might have a name
    greet <$> getLine                   -- apply your function to whatever you type in 
    greet <$> readFile "deletedId.csv"  -- apply your function to your file 
    

    最后一个是我们如何在readAndChange 中使用&lt;$&gt;。如果里面有很多数据 deletedId.csv 你会错过你好,但你当然可以这样做

    greet <$> readFile "deletedId.csv" >>= writeFile "hi.txt"
    take 4.lines <$> readFile "hi.txt"
    

    查看前 4 行。

    所以$ 让你可以在你给它的参数上使用你的函数。 greet :: String -&gt; String 所以如果你写greet $ personperson 必须是String 类型,而如果你写greet &lt;$&gt; someonesomeone 可以是产生String 的任何东西 - 字符串列表,IO StringMaybe String。从技术上讲,someone :: Applicative f =&gt; f String,但您应该首先阅读类型类和 Applicative Functors。 Learn You a Haskell for Great Good 是一个极好的资源。

    为了更有趣,如果你的函数有多个参数,你仍然可以使用可爱的 Applicative 样式。

    insult :: String -> String -> String
    insult a b = a ++ ", you're almost as ugly as " ++ b
    

    试试

    insult "Fred" "Barney"
    insult "Fred" $ "Barney"
    insult <$> ["Fred","Barney"] <*> ["Wilma","Betty"]
    insult <$> Just "Fred" <*> Nothing
    insult <$> Just "Fred" <*> Just "Wilma"
    insult <$> readFile "someone.txt" <*> readFile "someoneElse.txt"
    

    这里你在函数后面使用&lt;$&gt;,在它需要的参数之间使用&lt;*&gt;。一开始它的工作原理有点令人惊讶,但它是编写有效计算的最实用的风格。

    接下来阅读有关 Applicative Functors 的内容。他们很棒。
    http://learnyouahaskell.com/functors-applicative-functors-and-monoids
    http://en.wikibooks.org/wiki/Haskell/Applicative_Functors

    【讨论】:

    • 太棒了:) 你可以猜到我是个 Haskell 新手;如果$&lt;$&gt; 之间有什么区别,你能解释一下吗?
    • @Aslan986 我现在又添加了一点。
    • 不可能不接受你的回答。非常感谢安德鲁。
    【解决方案2】:
    import Control.Monad
    import Control.Monad.Trans
    import Control.Monad.Trans.Either
    
    readAndChange i o = do
        result <- fmap (either id id) $ runEitherT $ forever $ do
            iseof <- lift $ hIsEof i
            when iseof $ left o -- Break and return 'o'
            line <- lift $ hGetLine i
            lift $ hPutStrLn o $ show $ extractNameAndId line
        -- 'result' now contains the value of 'o' you ended on
        doSomeWithingWith result
    

    要了解此技术为何有效,请阅读this

    【讨论】:

      【解决方案3】:

      您可以使用let 模式进行递归,但类似于单独定义递归函数:

      main = do 
          let x = 10 
          let loop = do 
              print 1 
              when (x<20) loop 
          loop
      

      您也可以使用Control.Monad.Fix 中的fix 来实现类似的行为

      main = do 
          let x = 10 
          fix $ \loop -> do 
              print 1 
              when (x<20) loop
      

      您所说的是一种goto label 模式。我不知道您是否可以实现这种行为,但以上使用fixlet 可以轻松帮助您实现递归。

      [编辑] 还有更多的模式可以实现类似的结果,例如使用Cont monad

      getCC' :: MonadCont m => a -> m (a, a -> m b)
      getCC' x0 = callCC (\c -> let f x = c (x, f) in return (x0, f))
      
      main = (`runContT` return) $ do 
          (x, loopBack) <- getCC' 0
          lift (print x)
          when (x < 10) (loopBack (x + 1))
          lift (putStrLn "finish")
      

      这将打印来自1 to 10 的数字。请参阅goto using continuation 以获得更好的解释。

      还有一个Goto monad 和transformer。我没用过。您可能会发现它适合您的需要。

      【讨论】:

      • 我个人非常喜欢fix 方法。请注意,它在道德上等同于“递归let”方法,因为fix 是定义递归的一种方法。 (事实上​​,我只是将我的一些代码切换为使用fix 而不是递归的let。)另外,请注意fix 也可以在Data.Function 中使用;没有什么单子的。 Control.Monad.Fix提供了MonadFix类型类,它提供了mfix :: MonadFix m =&gt; (a -&gt; m a) -&gt; m a
      • 对于喜欢“循环组合器”方法的任何人,您可以在the monad-loops package 中找到比fix 更复杂的循环。这很整洁。
      【解决方案4】:

      您应该做的第一件事是阅读Control.Monad 模块的文档,这对于编写Haskell 代码是绝对必要的。当您使用它时,从 Hackage 安装 Control.Monad.Loops 软件包,并阅读相关文档;你可能对那里的whileM_ 函数特别感兴趣:

      import Data.Functor ((<$>)
      import Control.Monad.Loops (whileM_)
      
      readAndChange i o = 
          whileM_ notEOF $ do line <- hGetLine i
                              hPutStrLn o (show (extractNameAndId line))
              where notEOF = not <$> (hIsEOF i)
      

      有问题的库像这样实现whileM_,这是您正在寻找的模式:

      -- |Execute an action repeatedly as long as the given boolean expression
      -- returns True.  The condition is evaluated before the loop body.
      -- Discards results.
      whileM_ :: (Monad m) => m Bool -> m a -> m ()
      whileM_ p f = do
              x <- p
              if x
                      then do
                              f
                              whileM_ p f
                      else return ()
      

      不过,我不得不同意你写这篇文章的方式过于迫切。试着这样想:你的程序基本上是将输入字符串转换为输出字符串。这立即表明您的程序逻辑的核心应该具有这种类型:

      transformInput :: String -> String
      transformInput = ...
      

      您的转换是逐行进行的。这意味着您可以通过这种方式细化草图(lines 函数将字符串拆分为行;unlines 重新加入列表):

      transformInput :: String -> String
      transformInput input = unlines (map transformLine (lines input))
      
      transformLine :: String -> String
      transformLine line = show (extractNameAndId line)
      

      现在您已经掌握了transformInput 函数中的逻辑核心,因此您只需要将其连接到输入和输出句柄即可。如果您正在处理标准输入和标准输出,则可以使用 interact 函数来执行此操作。但我们实际上可以窃取它的实现并对其进行修改:

      hInteract       ::  Handle -> Handle -> (String -> String) -> IO ()
      hInteract i o f =   do s <- hGetContents i
                             hPutStr o (f s)
      

      现在,瞧:

      toCouplesFile = do inputFile <- openFile "deletedId.csv" ReadMode
                         outputFile <- openFile "couples.txt" WriteMode
                         hInteract inputFile outputFile transformInput
      

      警告:所有代码未经测试。


      最后一件事,为了全面披露:这里的技巧是 hGetContents 执行 惰性 I/O:它基本上允许您将句柄的全部内容视为 @ 987654339@,因此将transformInput 函数应用于句柄的内容。但这一切都是惰性完成的,因此它实际上不必一次读取整个文件。

      这是最简单的方法,你应该学习它,但它有一个很大的弱点,那就是当把手关闭时你可能会失去一些控制。但是,对于快速而肮脏的程序,这是可以的。

      【讨论】:

        【解决方案5】:

        与命令式编程语言不同,也与其他函数式编程语言不同,Haskell 不包含用于编写 for 循环或 while 循环的语法结构,而这正是您在这里所要求的。

        这个想法是递归过程和迭代过程可以统一地被递归函数捕获。只是迭代过程被捕获为特定种类的递归函数:这些函数是尾递归。诸如出现在 do-blocks 中的命令式代码也不例外。您可能会发现这种缺乏显式循环结构很烦人,因为您必须为每个循环定义一个新函数,因此在某种意义上必须为循环命名。然而,这对于 Haskell 方法的统一性和简单性来说是微不足道的,原因有以下三个:

        1. 您不必在顶层定义表示循环的函数。您可以在本地定义它。

        2. 在 Haskell 中,许多人通常总是对这些循环使用相同的名称。这里的热门选择是goaux。因此,您的代码可以重写如下:

          toCouplesFile = do inputFile <- openFile "deletedId.csv" ReadMode
                             outputFile <- openFile "couples.txt" WriteMode
                             let go = do
                                  iseof <- hIsEOF inputFile
                                  if iseof then (return outputFile)
                                  else do line <- hGetLine inputFile
                                          hPutStrLn outputFile (show (extractNameAndId line))
                                          go
                             go
          
        3. 最后,没有循环结构是无关紧要的,因为很多时候我们根本不需要编写循环。在您的情况下,此线程中的其他答案已经显示了许多这样做的方法。

        【讨论】:

          猜你喜欢
          • 2016-09-07
          • 2011-12-26
          • 2020-05-03
          • 2023-03-19
          • 1970-01-01
          • 2015-08-25
          • 1970-01-01
          • 2011-08-18
          • 2014-07-19
          相关资源
          最近更新 更多