在do块内迭代

时间:2012-09-11 19:30:50

标签: haskell monads

我写了这段代码:

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

5 个答案:

答案 0 :(得分:13)

你通过不必要的强制性编程来改变生活。您正在使用漂亮的Haskell语言进行编程,而您正在寻找goto构造!

为什么不只是import Control.Applicative (<$>)并写

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

(是的,这几乎是一个单行。它是干净的,功能性的,并且通过读写线的机制整洁。尽可能多的处理是在纯代码中完成的,只有输入和输出是IO-基础的。)

<强>解释

此处unlines.map (show.extractNameAndId).lines处理您的输入,方法是将其整理成行,然后使用extractNameAndIdshow然后map应用于每个输入,然后使用{{1}将它们重新连接在一起}}

unlines将读取文件并应用处理功能。 unlines.map (show.extractNameAndId).lines <$> readFile "deletedId.csv"<$>令人愉快的语法。

fmapwriteFile "couples.txt" =<< getanswer相同 - 按上述方式获取答案,然后将其写入文件。

尝试编写getanswer >>= writeFile "couples.txt"然后在ghci中为了好玩而做这些

greet xs = "hello " ++ xs

最后一个是我们在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 中使用<$>的方式。如果有很多数据 deletedId.csv你会错过你好,但你当然可以做到

readAndChange

查看前4行。

所以greet <$> readFile "deletedId.csv" >>= writeFile "hi.txt" take 4.lines <$> readFile "hi.txt" 允许你在你给它的参数上使用你的函数。 $所以如果你写greet :: String -> Stringgreet $ person必须是person类型,而如果你写Stringgreet <$> someone可以是产生someone的任何内容 - 字符串列表,StringIO String。从技术上讲,Maybe String,但您应该首先阅读类型类和Applicative Functors。了解一下Haskell for Great Good是一个很好的资源。

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

someone :: Applicative f => f String

尝试

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" ,在它需要的参数之间使用<$>。它的工作原理一开始有点令人兴奋,但它是编写有效计算的最具功能性的风格。

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

答案 1 :(得分:3)

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

答案 2 :(得分:3)

您可以使用let模式进行递归,但这与分别定义递归函数类似:

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

您也可以使用fix中的Control.Monad.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和变压器。我没用过它。您可能会发现它适合您的需要。

答案 3 :(得分:2)

您应该做的第一件事是阅读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函数中逻辑的核心,所以你只需要以某种方式将它连接到输入和输出句柄。如果你正在处理stdin和stdout,你可以使用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 :它基本上允许您将句柄的全部内容视为a String,因此将transformInput函数应用于句柄的内容。但这一切都是懒洋洋地完成的,所以它实际上不必立即读取整个文件。

这是最简单的方法,你应该学习它,但它有一个很大的弱点,那就是你可以在关闭句柄时失去一些控制权。但是,对于快速和脏的程序,这是可以的。

答案 4 :(得分:0)

与命令式编程语言不同,并且与其他函数式编程语言不同,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. 最后,缺少循环结构非常无关紧要,因为我们通常根本不需要编写循环。在您的情况下,此主题中的其他答案已经显示了许多方法。