Haskell 的 "do" 关键字有什么作用?

What does Haskell's "do" keyword do?

我是一名 C++/Java 程序员,我正在努力学习 Haskell(以及一般的函数式编程),而且我一直在粗略地学习它。我试过的一件事是:

isEven :: Int -> Bool
isEven x =
    if mod x 2 == 0 then True
    else False

isOdd :: Int -> Bool
isOdd x =
    not (isEven x)

main =
    print (isEven 2)
    print (isOdd 2)

但是在编译期间失败并出现此错误:

ghc --make doubler.hs -o Main
[1 of 1] Compiling Main             ( doubler.hs, doubler.o )

doubler.hs:11:5: error:
    • Couldn't match expected type ‘(a0 -> IO ()) -> Bool -> t’
              with actual type ‘IO ()’
    • The function ‘print’ is applied to three arguments,
      but its type ‘Bool -> IO ()’ has only one
      In the expression: print (isEven 2) print (isOdd 2)
      In an equation for ‘main’: main = print (isEven 2) print (isOdd 2)
    • Relevant bindings include main :: t (bound at doubler.hs:10:1)
make: *** [all] Error 1

所以,我在网上看到一些带有"do"关键字的代码,所以我试了一下:

isEven :: Int -> Bool
isEven x =
    if mod x 2 == 0 then True
    else False

isOdd :: Int -> Bool
isOdd x =
    not (isEven x)

main = do
    print (isEven 2)
    print (isOdd 2)

它的工作原理和我想象的完全一样。

这是怎么回事?为什么第一个代码片段不起作用?添加 "do" 到底有什么作用?

PS。我在网上看到了一些关于"monads"的东西,跟"do"关键字有关,跟这个有关系吗?

我想你暂时只能接受它。是的,do-notation 是 monad 类型 class 的语法糖。您的代码可以脱糖为以下内容:

main = print (isEven 2) >> print (isOdd 2)

(>>) 表示在这种特殊情况下,在此之后执行此操作。然而,试图在 Whosebug 答案中解释 Haskell IO 和 monad 确实没有什么好处。相反,我建议你继续学习,直到你的书或任何你用作学习资源的东西涵盖这个主题。

不过,这里有一个简单的示例,说明您可以在 IO-do 中执行的操作。不要太在意语法。

import System.IO
main = do
  putStr "What's your name? "  -- Print strings
  hFlush stdout                -- Flush output
  name <- getLine              -- Get input and save into variable name
  putStrLn ("Hello " ++ name)
  putStr "What's your age? "
  hFlush stdout
  age <- getLine
  putStr "In one year you will be "
  print (read age + 1)         -- convert from string to other things with read
                               -- use print to print things that are not strings

Why doesn't the first code snippet work?

do 块之外,换行符没有任何意义。所以你对 main 的第一个定义等同于 main = print (isEven 2) print (isOdd 2),它失败了,因为 print 只接受一个参数。

现在您可能想知道为什么我们不能只使用换行符来表示应该一个接一个地调用一个函数。问题在于 Haskell(通常)是惰性的并且是纯函数式的,因此函数没有副作用,并且没有一个接一个地调用函数的有意义的概念。

那么 print 是如何工作的呢? print 是一个接受字符串并生成 IO () 类型结果的函数。 IO 是一种表示可能产生副作用的操作的类型。 main 生成此类型的值,然后将执行该值描述的操作。虽然没有一个接一个地调用一个函数的有意义的概念,但是有一个接一个地执行一个 IO 值操作的有意义的概念。为此,我们使用 >> 运算符,它将两个 IO 值链接在一起。

I saw something about "monads" on the internet related to the "do" keyword, does that have something to do with this?

是的,Monad 是一种类型 class(如果您还不知道它们是什么:它们类似于 OO 语言中的接口),它(除其他外)提供函数 >>>>=IO 是该类型的一个实例 class(在 OO 术语中:一种实现该接口的类型),它使用这些方法将多个操作串联起来。

do 语法是使用 >>>>= 的更方便的方法。具体来说,您对 main 的定义等同于不带 do 的以下内容:

main = (print (isEven 2)) >> (print (isOdd 2))

(额外的括号不是必需的,但我添加它们是为了避免混淆优先级。)

所以main产生一个执行print (isEven 2)步骤的IO值,然后是print (isOdd 2).

的步骤

你知道一个函数的结果应该只取决于它的输入,所以让我们建模 print 来反映这一点:

print :: String -> RealWorld -> (RealWorld, ())

main 看起来像这样:

main rw0 = let (rw1, _) = print (isEven 2) rw0 in
                          print (isOdd 2) rw1

现在让我们定义 bind f g rw = let (rw', ret) = f rw in g rw' 来处理 RealWorld 状态并重写代码片段以使用它:

main = bind (print (isEven 2))
            (print (isOdd 2))

现在让我们介绍一些为我们binding

做的语法糖
main = do print (isEven 2)
          print (isOdd 2)

Haskell 函数是 "pure" 并且除了 "data dependencies" 之外没有排序的概念:用作另一个函数参数的函数的结果值。在基础层面上,没有要排序的语句,只有值。

有一个名为 IO 的类型构造函数。它可以应用于其他类型:IO IntIO CharIO StringIO sometype 表示:"this value is a recipe for doing some stuff in the real world and returning a value of sometype, once the recipe is executed by the runtime"。

这就是 main 类型为 IO () 的原因。你给出了在现实世界中做事的秘诀。 () 是一种只有一个值的类型,不提供任何信息。 main 仅因其在现实世界中的效果而被执行。

有许多运算符可用于组合 IO 个配方。一个简单的方法是 >>,它需要两个配方,returns 一个配方用于执行第一个配方,然后是第二个配方。请注意,组合是以纯粹的方式完成的,仅使用函数,即使复合配方实际上类似于命令式编程的顺序语句 ("Print this message, then this other message")。

为了简化这些 "imperative recipes" 的构造,创建了 do-notation。它允许您编写类似于命令式语言的顺序语句的东西,但随后它会脱糖为功能应用程序。你可以用 do-notation 写的所有东西,你都可以用常规函数应用程序来写(有时不太清楚)。