有时,如果我不使用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)
显示错误
答案 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 Int
或IO ()
),右侧为一个函数,该函数返回一元数值(例如Int -> IO String
),并且“他们在一起。结果是一个新的单子动作,该动作由一个接一个执行的两个输入动作组成。
将其应用于带有putStrLn
和countdown
的示例中,如下所示:
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)