7

我仍在为 Haskell 苦苦挣扎,现在我遇到了一个问题,即从这个示例中将注意力集中在 Input/Output monad 上:

main = do   
line <- getLine  
if null line  
    then return ()  
    else do  
        putStrLn $ reverseWords line  
        main  
  
reverseWords :: String -> String  
reverseWords = unwords . map reverse . words

我知道因为像 Haskell 这样的函数式语言不能基于函数的副作用,所以必须发明一些解决方案。在这种情况下,似乎所有东西都必须包装在一个do块中。我得到了简单的例子,但在这种情况下,我真的需要有人解释:

  1. do为什么对 I/O 操作使用一个单独的块还不够?
  2. 为什么你必须在 if/else 情况下打开一个全新的?
  3. 另外,什么时候——我不知道怎么称呼它do——monad的“范围”结束,即你什么时候可以使用标准的Haskell术语/函数?
4

2 回答 2

8

do块涉及与第一个语句处于相同缩进级别的任何内容。因此,在您的示例中,它实际上只是将两件事联系在一起:

 line <- getLine

以及所有其他的,恰好更大:

 if null line  
  then return ()
  else do
      putStrLn $ reverseWords line  
      main  

但无论多么复杂,do语法都不会考虑这些表达式。所以这一切都与

main :: IO ()
main = do
   line <- getLine
   recurseMain line

使用辅助功能

recurseMain :: String -> IO ()
recurseMain line
   | null line  = return ()
   | otherwise  = do
           putStrLn $ reverseWords line
           main

现在,显然里面的东西recurseMain不知道该函数是在domain 的一个块中调用的,所以你需要使用另一个do.

于 2014-06-18T20:12:22.207 回答
8

do实际上并没有做任何事情,它只是用于轻松组合语句的语法糖。一个可疑的类比do[]

如果您有多个表达式,您可以使用以下方法将它们组合成列表:

(1 + 2) : (3 * 4) : (5 - 6) : ...

然而,这很烦人,所以我们可以改用[]符号,它编译成同样的东西:

[1+2, 3*4, 5-6, ...] 

同样,如果您有多个 IO 语句,您可以使用and组合它们>>>>=

(putStrLn "What's your name?") >> getLine >>= (\name -> putStrLn $ "Hi " ++ name)

然而,这很烦人,所以我们可以改用do符号,它编译成同样的东西:

do
  putStrLn "What's your name?"
  name <- getLine
  putStrLn $ "Hi " ++ name

现在,为什么需要多个do块的答案很简单:

如果您有多个值列表,则需要多个[]s (即使它们是嵌套的)。

如果您有多个单子语句序列,则需要多个dos (即使它们是嵌套的)。

于 2014-06-18T20:12:47.037 回答