4

我写了这段代码:

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
4

5 回答 5

13

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

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

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

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

解释:

这里unlines.map (show.extractNameAndId).lines通过将输入切割成行来处理您的输入,extractNameAndId然后使用 应用show到每个输入,然后使用map将它们重新连接在一起unlines

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

writeFile "couples.txt" =<< getanswer是一样的getanswer >>= 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. 如果 deletedId.csv 中有很多数据,你会错过你好,但你当然可以

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

查看前 4 行。

所以$让你在你给它的参数上使用你的函数。greet :: String -> String所以如果你写greet $ person, theperson必须是 type String,而如果你写greet <$> someone, thesomeone可以是任何产生 a String-a 字符串列表,an IO String, a 的东西Maybe String。从技术上讲,someone :: Applicative f => 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"

在这里,您<$>在函数之后和<*>它需要的参数之间使用。一开始它的工作原理有点令人兴奋,但它是编写有效计算的最实用的风格。

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

于 2012-09-11T20:20:57.507 回答
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

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

于 2012-09-11T19:38:40.407 回答
3

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

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

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

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

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

[编辑] 还有一些模式可以实现类似的结果,例如使用Contmonad

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。我没用过。您可能会发现它适合您的需要。

于 2012-09-11T19:48:57.137 回答
2

您应该做的第一件事是阅读Control.Monad模块的文档,这对于编写 Haskell 代码是绝对必要的。当您使用它时,Control.Monad.Loops从 Hackage 安装软件包,并阅读相关文档;您可能对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:它基本上允许您将句柄的全部内容视为 a String,从而将transformInput函数应用于句柄的内容。但这一切都是懒惰地完成的,因此它实际上不必一次读取整个文件。

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

于 2012-09-11T23:12:15.177 回答
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. 最后,没有循环结构是无关紧要的,因为很多时候我们根本不需要编写循环。就您而言,此线程中的其他答案已经显示了许多这样做的方法。

于 2012-09-11T20:56:04.270 回答