如果函数式编程语言不能保存任何状态,那么它们如何做一些简单的事情,比如读取用户的输入(我的意思是他们如何“存储”它),或者存储任何数据?
正如您所收集的,函数式编程没有状态——但这并不意味着它不能存储数据。不同之处在于,如果我按照以下方式编写(Haskell)语句
let x = func value 3.14 20 "random"
in ...
我保证 : 中的值x
始终相同...
:没有什么可以改变它。同样,如果我有一个函数f :: String -> Integer
(一个接受字符串并返回一个整数的函数),我可以确定它f
不会修改它的参数,或更改任何全局变量,或将数据写入文件,等等。正如 sepp2k 在上面的评论中所说,这种不可变性对于推理程序非常有帮助:您编写折叠、旋转和破坏数据的函数,返回新副本以便您可以将它们链接在一起,并且您可以确定没有这些函数调用可以做任何“有害”的事情。你知道x
总是这样x
,而且你不必担心有人x := foo bar
在声明之间的某个地方写了x
以及它的用途,因为那是不可能的。
现在,如果我想读取用户的输入怎么办?正如 KennyTM 所说,不纯函数是一个纯函数,它将整个世界作为参数传递,并返回其结果和世界。当然,您并不想实际这样做:一方面,它非常笨重,另一方面,如果我重用同一个世界对象会发生什么?所以这以某种方式被抽象了。Haskell 使用 IO 类型处理它:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
这告诉我们这main
是一个不返回任何内容的 IO 操作;执行这个动作就是运行一个 Haskell 程序的意思。规则是 IO 类型永远不能逃脱 IO 动作;在这种情况下,我们使用do
. 因此,getLine
返回 an IO String
,可以通过两种方式来考虑:首先,作为一个动作,它在运行时会产生一个字符串;其次,作为一个被IO“污染”的字符串,因为它是不纯的。第一个更正确,但第二个可能更有帮助。取出<-
并存储它——但由于我们String
处于IO 操作中,我们必须将其包装起来,因此它不能“逃脱”。下一行尝试读取一个整数 ( ) 并获取第一个成功匹配 (IO String
str
reads
fst . head
); 这都是纯粹的(没有 IO),所以我们给它起一个let no = ...
. no
然后我们可以str
在...
. 因此,我们存储了不纯数据(从getLine
into str
)和纯数据(let no = ...
)。
这种使用 IO 的机制非常强大:它允许您将程序的纯算法部分与不纯的用户交互部分分开,并在类型级别强制执行。您的minimumSpanningTree
函数不可能在代码中的其他地方更改某些内容,或者向您的用户写一条消息,等等。它是安全的。
这是在 Haskell 中使用 IO 所需要知道的全部内容;如果这就是你想要的,你可以在这里停下来。但是,如果您想了解为什么会这样,请继续阅读。(请注意,这些内容将特定于 Haskell——其他语言可能会选择不同的实现。)
所以这可能看起来有点作弊,不知何故为纯 Haskell 添加了杂质。但事实并非如此——事实证明,我们可以完全在纯 Haskell 中实现 IO 类型(只要我们有RealWorld
. 想法是这样的:IO 动作IO type
与函数相同RealWorld -> (type, RealWorld)
,它接受现实世界并返回类型对象type
和修改后的对象RealWorld
。然后我们定义了几个函数,这样我们就可以使用这种类型而不会发疯:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
第一个允许我们谈论不做任何事情return 3
的 IO 动作:是一个不查询现实世界而只是返回的 IO 动作3
。操作符,发音为“ >>=
bind”,允许我们运行 IO 操作。它从 IO 动作中提取值,通过函数传递它和现实世界,并返回生成的 IO 动作。请注意,这>>=
强制执行我们的规则,即永远不允许 IO 操作的结果逃逸。
那么我们就可以把上面的main
变成如下普通的一组函数应用:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Haskell 运行时从main
初始 开始RealWorld
,我们准备好了!一切都是纯粹的,它只是有一个花哨的语法。
[编辑: 正如@Conal 指出的那样,这实际上并不是 Haskell 用来做 IO 的。如果您添加并发性,或者在 IO 操作中间添加任何改变世界的方式,这个模型就会失效,所以 Haskell 不可能使用这个模型。它仅对顺序计算是准确的。因此,Haskell 的 IO 可能有点躲闪;即使不是,它也肯定不是那么优雅。根据@Conal 的观察,请参阅 Simon Peyton-Jones 在Tackling the Awkward Squad [pdf]第 3.1 节中所说的话;他提出了沿着这些思路可能构成替代模型的东西,但随后因其复杂性而放弃了它并采取了不同的策略。]
同样,这(几乎)解释了 IO 和一般的可变性如何在 Haskell 中工作;如果这就是您想知道的全部内容,您可以在此处停止阅读。如果您想要最后一剂理论,请继续阅读 - 但请记住,此时,我们离您的问题已经很远了!
所以最后一件事:事实证明,这种结构——一个带有return
and的参数类型>>=
——是非常通用的;它被称为 monad,do
符号 ,return
和>>=
与它们中的任何一个一起使用。正如你在这里看到的,单子并不神奇。神奇的是do
块变成了函数调用。RealWorld
类型是我们看到任何魔法的唯一地方。列表构造函数之类的类型[]
也是 monad,它们与不纯代码无关。
您现在(几乎)了解有关 monad 概念的所有内容(除了一些必须满足的定律和正式的数学定义),但您缺乏直觉。网上有大量可笑的 monad 教程;我喜欢这个,但你有选择。但是,这可能对您没有帮助;获得直觉的唯一真正方法是结合使用它们并在正确的时间阅读一些教程。
但是,您不需要这种直觉来理解 IO。全面了解 monad 是锦上添花,但您现在可以使用 IO。在我向您展示了第一个main
功能之后,您就可以使用它了。您甚至可以将 IO 代码视为不纯语言!但请记住,有一个潜在的功能表示:没有人在作弊。
(PS:对不起,我走的有点远。)