什么时候应该使用do?

时间:2019-12-09 04:01:21

标签: haskell

有时,如果我不使用do,程序会显示错误。但是它有时运行良好,却没有做。 例如:

countdown ::Int -> IO ()
countdown x =  if x <= 0 
               then putStrLn "The End."
               else putStrLn (show (x))

运行良好 但是

countdown ::Int -> IO ()
countdown x =  if x <= 0 
               then putStrLn "The End."
               else putStrLn (show (x)) 
                    countdown (x-1)

显示错误

1 个答案:

答案 0 :(得分:18)

简短的回答:在Haskell中,换行符并不意味着“下一条语句”,就像在许多主流语言(如Python,Ruby和最近甚至JavaScript)中一样。


好答案

首先,让我们摆脱if。只会笼罩问题:

countdown1 :: Int -> IO ()
countdown1 x = putStrLn (show x)

countdown2 :: Int -> IO ()
countdown2 x = putStrLn (show x)
               countdown (x-1)

请注意,您的函数类型为IO ()。这就是函数最终必须计算的类型。如果愿意,也可以“生产”。这意味着等号右边的任何内容都必须为IO ()类型。

countdown x = .........
              ^       ^
              +-------+
                  \
                   the type of whatever is here must be `IO ()`

第一个版本countdown1确实满足了这一要求,因为表达式putStrLn (show x)的类型确实为IO ()

另一方面,第二个版本countdown2对于编译器来说看起来很奇怪。看来您正在尝试调用putStrLn,并且正在尝试传递三个参数:(show x)countdown(x-1)

不要让(show x)countdown之间的换行符使您感到困惑:如上所述,换行符在Haskell中并不表示“下一条语句”。这是因为在Haskell中,一开始就没有“声明”之类的东西。一切都是表情。

但是等等!如果换行符不算作“下一条语句”,那么我该如何告诉Haskell按顺序执行几个动作?例如,首先执行putStrLn,然后执行countdown

好吧,这就是单子的出现。单子(其中IO是一个典型的例子)是出于以下原因而专门引入Haskell的:表达事物的顺序。并且主要的操作是“ bind” ,在Haskell中以运算符>>=的形式存在。此运算符的左侧为一元数值(例如IO IntIO ()),右侧为一个函数,该函数返回一元数值(例如Int -> IO String),并且“他们在一起。结果是一个新的单子动作,该动作由一个接一个执行的两个输入动作组成。

将其应用于带有putStrLncountdown的示例中,如下所示:

putStrLn (show x) >>= (\y -> countdown (x-1))
^               ^     ^                     ^
+---------------+     +---------------------+
    \                        \
     first monadic value      a function that takes the result of the
                              first action as parameter and returns
                              the second action

但这有点不方便。当然,您可以将两个动作粘合在一起,甚至三个。但是一段时间后,这变得非常混乱。 (起初我在这里有一个示例,但后来决定不这样做;请相信我:它确实很乱)

因此,为了减轻混乱,该语言现在以do表示法的形式提供了语法糖。在do表示法中,换行符确实的意思是“下一条语句”,而这些连续的“语句”被分解为对>>=运算符的一系列调用,给他们“顺序执行”的语义。像这样:

do
  y <- f x
  z <- h y            ====>      f x >>= (\y -> h y >>= (\z -> g x y z))
  g x y z

因此,do表示法看上去与Python,Ruby或JavaScript中的多行程序大致等效。但在这一切之下,它仍然是一个纯函数式程序,其中的所有内容都是一个表达式,并且(非纯粹有效的)操作的顺序受到显式控制。


因此,总结一下:您需要在程序中使用do来表示顺序-首先是putStrLn,然后是countdown

countdown :: Int -> IO ()
countdown x = if x <= 0 
              then putStrLn "The End."
              else do
                     putStrLn (show (x)) 
                     countdown (x-1)

但是,当您只执行一项操作时,您不需要使用do,因此无需多说。

如果出于某种原因不想使用do,则可以将其手动解糖为等效的>>=调用:

countdown :: Int -> IO ()
countdown x = if x <= 0 
              then putStrLn "The End."
              else putStrLn (show (x)) >>= \_ -> countdown (x-1)