为什么 Haskell 需要 IO/Actions,即使它是惰性求值?

Fun*_*tor 0 haskell

我想这可能是一个有争议的话题,因为我深入接触了语言设计,而且我知道周围有些人不会喜欢这样,因为他们误解了我否认了他们喜欢的东西的某些优点。

\n

为什么 Haskell 需要IO/Actions,即使它是惰性求值

\n

我理解 IO/Actions 机制的价值,如果它是针对 C、JavaScript 或任何其他语言等急于评估的语言,那么它可以保持函数式编程的所谓“纯度”。

\n

事实上,我确实IO ()在 Typescript 中模拟/实现了急切的求值,然后我想“好吧,很酷,但是为什么 Haskell 需要这个?”

\n

Haskell 默认情况下是惰性的,因此即使函数也被定义为

\n

print==console.log

\n

在 JavaScript 语法中,在 Haskell 中,因为它是惰性的,print所以除非它连接到main :: IO ().

\n

有什么想法吗?

\n

编辑:

\n

显然,这个问题源于我的完全误解。

\n

在 Haskell 中,它定义为

\n

print==console.log

\n

print :: Show a => a -> IO () -- Defined in \xe2\x80\x98System.IO\xe2\x80\x99

\n

我只是误解了好像定义为

\n

print :: Show a => a -> _ -> IO ()

\n

因为在急切的评估中需要如此模仿。

\n

Dan*_*ner 11

你把事情搞混了!Haskell 不需要IO尽管懒惰,因为懒惰而需要它。

让我们想象一下我们没有IO(或者,等效地,所做的一切都IO被 隐式包装unsafePerformIO)。因此,举例来说,我可能会写:

main = print (readLn + readLn)
Run Code Online (Sandbox Code Playgroud)

这将从用户处获取两行输入,将它们解析为数字,将它们相加,然后打印结果。好的!到目前为止没有问题。现在我决定要实现一种小语言。我想做的事情是从用户那里读取几个(比如 5 个)变量/值对,将它们粘贴在 a 中Map,然后从用户那里读取可能提到这些变量的表达式。因此与用户的交互可能看起来像

> 5
> 32
> 17
> -6
> 72
> (x1 + x4) * (x0 + x3)
< -104
Run Code Online (Sandbox Code Playgroud)

其中>标记我输入的行并<标记程序打印的行。答案是 -104,因为 x1=32、x4=72、x0=5 和 x3=-6。不使用 x2=17 的绑定。好吧,我们写一下吧。

import qualified Data.Map as M

interpret :: M.Map String Int -> String -> Int
interpret = {- not relevant, really... right? -}

main = interpret env expr where
    env = M.fromList [("x0", readLn), ("x1", readLn), ("x2", readLn), ("x3", readLn), ("x4", readLn)]
    expr = getLine
Run Code Online (Sandbox Code Playgroud)

好的,现在,小测验:这个程序是做什么的?好吧,如果我们认真对待懒惰,那么所有这些getLine都会被推迟,直到有人真正看到它们为止。如果有人在看,那是谁?它是interpret!因此,要知道这个程序的作用,我们实际上必须知道它的interpret作用。好的,我们开始填写:

interpret env s = case parseExpr s of
    Just expr -> evaluateArithmetic (replaceVariables env expr)
    Nothing -> 404 -- lol
Run Code Online (Sandbox Code Playgroud)

...aaaand,现在我们有麻烦了。实际上,出于多种原因。因为首先interpret要做的是计算s,这意味着用户输入的第一行实际上扮演了表达式的角色,而不是最后一行。所以这有点不幸,但是好吧,也许我们只是认为这很好并重新构想我们的理想交互以符合这些实现细节:

> (x1 + x4) * (x0 + x3)
> 5
> 32
> 17
> -6
> 72
< -104
Run Code Online (Sandbox Code Playgroud)

但即使我们放弃将表达式放在最后的梦想,我们仍然遇到麻烦。因为看看replaceVariables会发生什么:

data Expr = Lit Int | Var String | Add Expr Expr | Times Expr Expr

replaceVariables env (Lit n) = Lit n
replaceVariables env (Var v) = Lit (env M.! v)
replaceVariables env (Add x y) = Add (replaceVariables env x) (replaceVariables env y)
replaceVariables env (Times x y) = Times (replaceVariables env x) (replaceVariables env y)
Run Code Online (Sandbox Code Playgroud)

你发现了吗?用户输入的表达式中,x1是它尝试替换的第一个变量——这意味着它是第一个readLn被执行的变量,我们输入的第二个数字不是 32,而是我们想要的,它是 5,第一个数字我们输入的号码。类似地,x4 变成 32 而不是 72,等等,我们得到一个完全错误的答案。(另外,程序会在我们输入第四个数字后回复,而无需等待第五个数字。但也许这没什么大不了的。)

所以这就是问题的关键:如果没有IO,程序员对与用户交互发生的顺序的控制就会少得多。还有一个我们在这里没有探讨的后续问题,那就是不仅控制很少,但是重构可以改变界面——如果我们出于某种原因replaceVariables交换参数Add,即使这看起来确实是一个不会影响任何东西的改变,它也会使得从用户处读取行的顺序更加不同令人困惑!

这是要解决的核心问题IO。的实现(>>=)添加了数据依赖性,阻止后面的计算在前面的计算完成之前执行。这意味着当我们写

main = readLn >>= \x -> {- rest of the program -}
Run Code Online (Sandbox Code Playgroud)

我们可以确定它x包含用户输入的第一行的内容,而不是由程序的整个其余部分的结构确定的其他行。

必须立即理解整个程序才能知道它的小块的作用,这在大规模上是行不通的!