简体   繁体   English

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

[英]What does Haskell's “do” keyword do?

I'm a C++/Java programmer, and I'm trying to learn Haskell (and functional programming in general), and I've been having a rough go at it. 我是一名C ++ / Java程序员,我正在努力学习Haskell(以及一般的函数式编程),而且我一直在努力学习它。 One thing I tried was this: 我试过的一件事是这样的:

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)

But this failed with this error during compilation: 但是在编译期间这个错误失败了:

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

So, I saw some code online with the "do" keyword, so I tried it like this: 所以,我在网上看到了一些带有“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)

And it worked exactly like I thought it should. 它的工作方式与我认为应该完全一样。

What's going on here? 这里发生了什么? Why doesn't the first code snippet work? 为什么第一个代码段不起作用? And what does adding "do" actually do? 添加“做”实际上做了什么?

PS. PS。 I saw something about "monads" on the internet related to the "do" keyword, does that have something to do with this? 我在互联网上看到了与“do”关键字相关的“monads”,这与此有关吗?

Why doesn't the first code snippet work? 为什么第一个代码段不起作用?

Outside of a do block, line breaks don't have any significance. do块之外,换行没有任何意义。 So your first definition of main is equivalent to main = print (isEven 2) print (isOdd 2) , which fails because print only takes one argument. 因此, main的第一个定义等同于main = print (isEven 2) print (isOdd 2) ,它因为print只接受一个参数而失败。

Now you may wonder why we can't just use line breaks to signify that one function should be called after another. 现在你可能想知道为什么我们不能只使用换行符来表示应该一个接一个地调用一个函数。 The problem with that is that Haskell is (usually) lazy and purely functional, so functions don't have side-effects and there's no meaningful concept of calling one function after another. 问题在于Haskell(通常)是懒惰的并且纯粹是功能性的,因此函数没有副作用,并且没有任何有意义的概念来调用一个接一个的函数。

So then how does print work at all? 那么print如何工作呢? print is a function that takes a string and produces a result of type IO () . print是一个函数,它接受一个字符串并产生IO ()类型的结果。 IO is a type that represents possibly side-effecting operations. IO是一种表示可能产生副作用的操作的类型。 main produces a value of this type and the operations described by that value will then be executed. main生成此类型的值,然后将执行该值描述的操作。 And while there's no meaningful concept of calling one function after another, there is a meaningful concept of executing one IO value's operation after another one's. 虽然没有一个接一个地调用一个函数的有意义的概念,但是有一个有意义的概念是在另一个函数之后执行一个IO值的操作。 For this we use the >> operator, which chains two IO values together. 为此,我们使用>>运算符,它将两个IO值链接在一起。

I saw something about "monads" on the internet related to the "do" keyword, does that have something to do with this? 我在互联网上看到了与“do”关键字相关的“monads”,这与此有关吗?

Yes, Monad is a type class (if you don't know what those are yet: they're similar to interfaces in OO languages), which (among others) provides the functions >> and >>= . 是的, Monad是一个类型类(如果你还不知道它们是什么:它们类似于OO语言中的接口),其中(以及其他)提供了函数>>>>= IO is one instance of that type class (in OO terms: one type that implements that interface), which uses those methods to chain multiple operations after each other. IO是该类型类的一个实例(在OO术语中:一种实现该接口的类型),它使用这些方法将多个操作链接在一起。

The do syntax is a more convenient way of using >> and >>= . do语法是使用>>>>=的更方便的方法。 Specifically your definition of main is equivalent to the following without do : 具体来说,你的main定义相当于以下内容而不do

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

(The extra parentheses aren't necessary, but I added them to avoid any confusion about precedence.) (额外的括号不是必需的,但我添加了它们以避免对优先级产生任何混淆。)

So main produces an IO value that executes the steps of print (isEven 2) , followed by those of print (isOdd 2) . 因此main生成一个IO值,执行print (isEven 2) ,然后是print (isOdd 2)

I think for the time being you will just have to accept it. 我认为暂时你只需要接受它。 Yes, the do -notation is syntactic sugar for the monad type class. 是的, do -notation是monad类型的语法糖。 Your code could be desugared to the following: 你的代码可以满足以下要求:

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

(>>) means something like do this after that in this particular case. (>>)表示在此特定情况下之后执行的操作。 However there is really no good in trying to explain Haskell IO and monads in a StackOverflow answer. 然而,尝试在StackOverflow答案中解释Haskell IO和monads确实没有用。 Instead I recommend you to just keep learning until your book or whatever you use as a learning resource covers the topic. 相反,我建议你继续学习,直到你的书或任何你作为学习资源使用的内容涵盖主题。

Here is however a quick example of what you can do inside of IO - do . 这里然而,你可以在里面做什么一个简单的例子IO - do Don't bother about the syntax too much. 不要太在意语法。

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

Haskell functions are "pure" and have no notion of sequencing apart from "data dependencies": the result value of a function used as argument to another. Haskell函数是“纯粹的”,除了“数据依赖性”之外没有排序的概念:函数的结果值用作另一个函数的参数。 At the basic level, there are no statements to be sequenced, only values. 在基本级别,没有要排序的语句,只有值。

There is a type constructor called IO . 有一个名为IO的类型构造函数。 It can be applied to other types: IO Int , IO Char , IO String . 它可以应用于其他类型: IO IntIO CharIO String IO sometype means: "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". IO sometype意味着:“这个值是在现实世界中做一些事情并且在运行时执行配方后返回sometype值的配方”。

That's why main has type IO () . 这就是main具有类型IO () You give a recipe for doing stuff in the real world. 你给出了在现实世界中做事的秘诀。 () is a type with only one value, which provides no information. ()是只有一个值的类型,它不提供任何信息。 main is executed only for its effects in the real world. main仅针对其在现实世界中的效果执行。

There are a number of operators for combining IO recipes. 有许多运算符用于组合IO配方。 A simple one is >> that takes two recipes and returns a recipe for executing the first recipe, then the second. 一个简单的是>> ,它采用两个配方并返回执行第一个配方的配方,然后是第二个配方。 Notice that the combination is done in a pure way, using mere functions, even if the composite recipe actually resembles the sequential statements of imperative programming ("Print this message, then this other message"). 请注意,即使复合配方实际上类似于命令式编程的顺序语句(“打印此消息,然后是其他消息”),也只能使用单纯的函数以纯粹的方式完成组合。

To simplify the construction of these "imperative recipes", do-notation was created. 为了简化这些“命令式食谱”的构建,创建了符号 It lets you write something resembling the sequential statements of an imperative language, but then it desugars to function applications. 它允许你编写类似于命令式语言的顺序语句的东西,但后来它开始运行应用程序。 Everything you can write in do-notation, you can write (sometimes less clearly) with regular function application. 您可以使用常规函数编写所有内容,您可以使用常规函数应用程序编写(有时不太清楚)。

You know that the result of a function should only depend on its input, so let's model print to reflect that: 你知道函数的结果应该只取决于它的输入,所以让我们的模型print来反映:

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

main would then look like this: main会看起来像这样:

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

Now let's define bind fg rw = let (rw', ret) = f rw in g rw' which does this handing through of the RealWorld state and rewrite the snippet to use it: 现在让我们定义bind fg rw = let (rw', ret) = f rw in g rw' ,它执行RealWorld状态并重写代码片段以使用它:

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

Now let's introduce some syntax sugar that does the bind ing for us 现在让我们介绍一些为我们bind语法糖

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM