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

时间:2016-11-27 18:14:17

标签: haskell syntax io main keyword

我是一名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)

它的工作方式与我认为应该完全一样。

这里发生了什么?为什么第一个代码段不起作用?添加“做”实际上做了什么?

PS。我在互联网上看到了与“do”关键字相关的“monads”,这与此有关吗?

4 个答案:

答案 0 :(得分:13)

  

为什么第一个代码段不起作用?

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

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

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

  

我在互联网上看到与“do”关键字有关的“monads”,这与此有关吗?

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

do语法是使用>>>>=这些函数的更方便的方法。具体来说,您对main的定义等同于以下而没有do

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

(额外的括号不是必需的,但我添加了它们以避免对优先级产生任何混淆。)

因此main生成一个IO值,执行print (isEven 2)的步骤,然后执行print (isOdd 2)的步骤。

答案 1 :(得分:4)

我认为暂时你必须接受它。是的,do - 符号是monad类型的语法糖。您的代码可能会出现以下情况:

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

(>>)意味着在此特定情况下执行此类操作。然而,尝试在StackOverflow答案中解释Haskell IO和monads确实没有用。相反,我建议你继续学习,直到你的书或任何你用作学习资源的内容为止。

然而,这是您在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

答案 2 :(得分:3)

Haskell函数是“纯粹的”,除了“数据依赖”之外没有排序的概念:函数的结果值用作另一个函数的参数。在基础层面,没有要排序的陈述,只有值。

有一个名为IO的类型构造函数。它可以应用于其他类型:IO IntIO CharIO StringIO sometype表示:“这个值是在现实世界中做某些事情并且在运行时执行配方后返回值sometype的配方”。

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

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

为了简化这些“命令性配方”的构建,创建了 do-notation 。它允许你编写类似于命令式语言的顺序语句的东西,但后来它开始运行应用程序。您可以使用常规函数编写所有内容,您可以使用常规函数应用程序编写(有时不太清楚)。

答案 3 :(得分:0)

你知道函数的结果应该只取决于它的输入,所以让模型Access-Control-Max-Age来反映:

Access-Control-Max-Age: 1728000

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))