为什么 Haskell 需要有 IO/Actions 即使它是惰性评估?
Why does Haskell need to have IO/Actions even though it's lazy-evaluation?
我想这可能是一个有争议的话题,因为我深入接触了语言设计,而且我知道周围的一些人不会喜欢那样,因为他们误解了我否认他们喜欢的东西的某些优点。
为什么Haskell需要有IO/Actions even though it's lazy-evaluation?
我理解 IO/Actions 机制的价值,如果它是用于急切求值的语言,例如 C、JavaScript 或任何其他语言,那么它可以保持函数式编程的所谓“纯度”。
事实上,我在热切求值的 Typescript 中做了 emulate/implement IO ()
,然后我想“好吧,很酷,但是为什么 Haskell 需要这个??”
Haskell默认是惰性的,因此即使函数定义为
print
== console.log
在JavaScript语法中,在Haskell中因为它是惰性的,所以print
无论如何都不会被执行,除非它连接到main :: IO ()
。
有什么想法吗?
编辑:
显然,这个问题完全是我的误解引起的。
在Haskell中定义为
print
== console.log
print :: Show a => a -> IO () -- Defined in ‘System.IO’
我简单理解为好像定义为
print :: Show a => a -> _ -> IO ()
因为在急切的评估中需要这样做才能效仿。
你搞混了! Haskell 在 尽管 懒惰中不需要 IO
,它需要它 因为 懒惰。
让我们想象一下,我们没有 IO
(或者,等效地,IO
的所有内容都被 unsafePerformIO
隐式包装)。因此,例如,我可能会写:
main = print (readLn + readLn)
这将从用户那里获得两行输入,将它们解析为数字,将它们相加,然后打印结果。好的!到目前为止没问题。现在我决定要实现一种小语言。我想做的是从用户那里读取一对——比如 5 对 variable/value 对,将它们放在 Map
中,然后从用户那里读取可能提到这些的表达式变量。所以与用户的交互可能看起来像
> 5
> 32
> 17
> -6
> 72
> (x1 + x4) * (x0 + x3)
< -104
其中 >
标记我输入的行,<
标记程序打印的行。答案是 -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
好的,现在,小测验:这个程序是做什么的?好吧,如果我们认真对待懒惰,那么所有这些 getLine
都会被推迟,直到有人真正查看它们。如果有人在看,那是谁?是interpret
!所以,要知道这个程序做了什么,我们实际上必须知道 interpret
做了什么。好了,开始填写吧:
interpret env s = case parseExpr s of
Just expr -> evaluateArithmetic (replaceVariables env expr)
Nothing -> 404 -- lol
...aaaand,现在我们有麻烦了。实际上,出于 一堆 的原因。因为 interpret
做的第一件事是计算 s
,这意味着 用户键入的第一行实际上扮演了表达式的角色,而不是最后一行 。所以这有点不幸,但是好吧,也许我们只是认为这很好并重新构想我们理想的交互以符合这些实现细节:
> (x1 + x4) * (x0 + x3)
> 5
> 32
> 17
> -6
> 72
< -104
但即使我们放弃将表达式放在最后的梦想,我们仍然有麻烦。因为看看 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)
你发现了吗?对于用户输入的表达式,x1
是它尝试替换的第一个变量——这意味着 它是第一个被执行的 readLn
,而是32,我们输入的第二个数字,正如我们预期的那样,它是 5,我们输入的第一个数字。同样,x4 变成 32 而不是 72,等等,我们得到一个完全错误的答案。 (此外,程序会在我们输入第四个数字后回复,而无需等待第五个数字。但也许这没什么大不了的。)
所以这就是问题的症结所在:如果没有 IO
,程序员对与用户进行交互的顺序的控制就会少得多。有一个 follow-on 我们没有遇到的问题探索这里,这不仅是几乎没有控制,而且重构可以改变接口——如果我们让replaceVariables
将参数交换为Add
出于某种原因,尽管这看起来确实是一个不应该影响任何事情的更改,但它使得从用户那里读取行的顺序更加不同和令人困惑!
这是IO
解决的核心问题。 (>>=)
的实现添加了一个数据依赖性,可以防止后面的计算在前面的计算完成之前执行。这意味着当我们写
main = readLn >>= \x -> {- rest of the program -}
我们可以确定 x
包含用户输入的第一行的内容,而不是由整个程序其余部分的结构决定的其他行。
必须一次理解整个程序才能知道它的一小部分在大规模工作是行不通的!
我想这可能是一个有争议的话题,因为我深入接触了语言设计,而且我知道周围的一些人不会喜欢那样,因为他们误解了我否认他们喜欢的东西的某些优点。
为什么Haskell需要有IO/Actions even though it's lazy-evaluation?
我理解 IO/Actions 机制的价值,如果它是用于急切求值的语言,例如 C、JavaScript 或任何其他语言,那么它可以保持函数式编程的所谓“纯度”。
事实上,我在热切求值的 Typescript 中做了 emulate/implement IO ()
,然后我想“好吧,很酷,但是为什么 Haskell 需要这个??”
Haskell默认是惰性的,因此即使函数定义为
print
== console.log
在JavaScript语法中,在Haskell中因为它是惰性的,所以print
无论如何都不会被执行,除非它连接到main :: IO ()
。
有什么想法吗?
编辑:
显然,这个问题完全是我的误解引起的。
在Haskell中定义为
print
== console.log
print :: Show a => a -> IO () -- Defined in ‘System.IO’
我简单理解为好像定义为
print :: Show a => a -> _ -> IO ()
因为在急切的评估中需要这样做才能效仿。
你搞混了! Haskell 在 尽管 懒惰中不需要 IO
,它需要它 因为 懒惰。
让我们想象一下,我们没有 IO
(或者,等效地,IO
的所有内容都被 unsafePerformIO
隐式包装)。因此,例如,我可能会写:
main = print (readLn + readLn)
这将从用户那里获得两行输入,将它们解析为数字,将它们相加,然后打印结果。好的!到目前为止没问题。现在我决定要实现一种小语言。我想做的是从用户那里读取一对——比如 5 对 variable/value 对,将它们放在 Map
中,然后从用户那里读取可能提到这些的表达式变量。所以与用户的交互可能看起来像
> 5
> 32
> 17
> -6
> 72
> (x1 + x4) * (x0 + x3)
< -104
其中 >
标记我输入的行,<
标记程序打印的行。答案是 -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
好的,现在,小测验:这个程序是做什么的?好吧,如果我们认真对待懒惰,那么所有这些 getLine
都会被推迟,直到有人真正查看它们。如果有人在看,那是谁?是interpret
!所以,要知道这个程序做了什么,我们实际上必须知道 interpret
做了什么。好了,开始填写吧:
interpret env s = case parseExpr s of
Just expr -> evaluateArithmetic (replaceVariables env expr)
Nothing -> 404 -- lol
...aaaand,现在我们有麻烦了。实际上,出于 一堆 的原因。因为 interpret
做的第一件事是计算 s
,这意味着 用户键入的第一行实际上扮演了表达式的角色,而不是最后一行 。所以这有点不幸,但是好吧,也许我们只是认为这很好并重新构想我们理想的交互以符合这些实现细节:
> (x1 + x4) * (x0 + x3)
> 5
> 32
> 17
> -6
> 72
< -104
但即使我们放弃将表达式放在最后的梦想,我们仍然有麻烦。因为看看 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)
你发现了吗?对于用户输入的表达式,x1
是它尝试替换的第一个变量——这意味着 它是第一个被执行的 readLn
,而是32,我们输入的第二个数字,正如我们预期的那样,它是 5,我们输入的第一个数字。同样,x4 变成 32 而不是 72,等等,我们得到一个完全错误的答案。 (此外,程序会在我们输入第四个数字后回复,而无需等待第五个数字。但也许这没什么大不了的。)
所以这就是问题的症结所在:如果没有 IO
,程序员对与用户进行交互的顺序的控制就会少得多。有一个 follow-on 我们没有遇到的问题探索这里,这不仅是几乎没有控制,而且重构可以改变接口——如果我们让replaceVariables
将参数交换为Add
出于某种原因,尽管这看起来确实是一个不应该影响任何事情的更改,但它使得从用户那里读取行的顺序更加不同和令人困惑!
这是IO
解决的核心问题。 (>>=)
的实现添加了一个数据依赖性,可以防止后面的计算在前面的计算完成之前执行。这意味着当我们写
main = readLn >>= \x -> {- rest of the program -}
我们可以确定 x
包含用户输入的第一行的内容,而不是由整个程序其余部分的结构决定的其他行。
必须一次理解整个程序才能知道它的一小部分在大规模工作是行不通的!