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

问题描述:

我是一名C++/Java程序员,我正在努力学习Haskell(以及一般的函数式编程),并且我一直在努力。有一两件事我想是这样的:Haskell的“do”关键字有什么作用?

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 

于是,我看到了一些代码,在网上与“做”的关键字,所以我想它像这样:

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) 

它的工作原理和我以为应该完全一样。

这是怎么回事?为什么第一个代码片段不起作用?添加“做”实际上是做什么的?

PS。我在互联网上看到有关“do”关键字的“单子”,这是否与此有关?

+0

我觉得这个问题太宽泛了,你最好去阅读一本haskell书中的'Monad'章节。您可以通过在本网站或Google上搜索“desugaring do notation haskell”来回答您的问题,但您可能需要更多背景信息 – jberryman

+3

['do' notation](https://en.wikibooks.org/wiki/ Haskell/do_notation)是普通一元代码的糖。如果没有'do',你可以写'main = print(isEven 2)>> print(isOdd 2)'。 – Alec

为什么不第一代码片段工作?

do块之外,换行符没有任何意义。因此,您的main的第一个定义相当于main = print (isEven 2) print (isOdd 2),由于print只接受一个参数,因此该定义失败。

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

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

我在互联网上看到有关“do”关键字的“单子”,这是否与此有关?

是,Monad是一种类(如果你不知道这些尚未:他们是类似于OO语言界面),它(等等)提供的功能>>>>=IO是该类型类的一个实例(以OO术语:实现该接口的一种类型),它使用这些方法将多个操作彼此链接起来。

do语法是使用>>>>=这些函数的更方便的方法。特别是你的主要定义等同于以下不do

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

(额外的括号是没有必要的,但我加入他们,以避免有关优先混淆。)

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

我认为暂时你只需要接受它。是的,do -notation是monad类型的语法糖。您的代码可以被脱到以下几点:

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

(>>)意味着像这样做后,在这种特殊情况下。然而,在尝试解释*答案中的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 
+1

你可以使用多于两条语句的“do”语法吗? – Dovahkiin

+2

是的,你可以尽可能多地使用它。 – jpath

+1

您可能希望在putStr调用之后添加'hFlush stdout',以确保在行缓冲打开时显示提示。 –

你知道一个函数的结果应该只依赖于它的输入,让我们的模型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)) 

现在让我们来介绍一些语法糖,做了bind荷兰国际集团为我们

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

Haskell函数是“纯粹”的,并没有测序的概念,除了“数据依赖”:使用的函数的结果值作为另一个论点。在基本层面上,没有要排序的语句,只有值。

有一个叫做IO的类型构造函数。它可以应用于其他类型:IO Int,IO Char,IO StringIO sometype意思是:“这个值是在现实世界中做一些东西的配方,并且一旦配方由运行时执行,返回值为sometype”。

这就是为什么main的型号为IO()。你给现实世界中的东西配方。 ()是只有一个值的类型,它不提供任何信息。 main仅针对其在现实世界中的效果而被执行。

有一些运营商结合IO食谱。一个简单的方法是>>需要两个配方,并返回执行第一个配方的配方,然后返回第二个配方。注意,即使复合配方实际上类似于命令式编程的顺序语句(“打印此消息,然后是其他消息”),也可以纯粹的方式使用单纯的函数完成组合。

为了简化这些“命令式食谱”的构建,编号已创建。它可以让你编写类似于命令式语言的顺序语句的东西,但它可以解析函数应用程序。你可以用符号来写所有的东西,你可以用常规的函数应用来写(有时不太清楚)。