如果函数式编程语言无法保存任何状态,它们将如何做一些简单的事情,例如从用户那里读取输入(我的意思是他们如何“存储”它)或为此存储任何数据?
正如您所收集的那样,函数式编程没有状态-但这并不意味着它不能存储数据。区别在于,如果我按照以下方式写(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 String
,并将其存储在str
-但因为我们是在一个IO动作,我们必须把它包备份,所以它也不能“免俗”。下一行尝试读取整数(reads
),并获取第一个成功匹配项(fst . head
); 这都是纯的(没有IO),因此我们给它起一个名字let no = ...
。然后,我们可以在no
和str
中使用...
。因此,我们已经存储了不纯数据(从getLine
到str
)和纯数据(let no = ...
)。
这种用于IO的机制非常强大:它使您可以将程序的纯算法部分与不纯的用户交互部分分开,并在类型级别上加以实施。您的minimumSpanningTree
函数可能无法更改代码中的其他内容,也无法向用户写消息,依此类推。它是安全的。
这就是在Haskell中使用IO所需要知道的一切。如果仅此而已,可以在这里停止。但是,如果您想了解它为什么起作用,请继续阅读。(请注意,这些内容将特定于Haskell,其他语言可能会选择其他实现。)
因此,这似乎有点作弊,以某种方式在纯净的Haskell中添加了杂质。但事实并非如此-事实证明,我们可以完全在纯Haskell中实现IO类型(只要获得RealWorld
)即可。想法是这样的:IO操作IO type
与函数相同RealWorld -> (type, RealWorld)
,后者采用真实世界并返回type type
和Modifyed 对象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
。>>=
称为“绑定” 的操作员允许我们运行IO操作。它从IO操作中提取值,通过函数将其传递给现实世界,然后返回结果IO操作。请注意,>>=
执行我们的规则是永远不允许逸出IO操作的结果。
然后,我们可以将以上内容main
转换为以下普通的功能应用程序集:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Haskell运行时从main
initial开始RealWorld
,我们就开始了!一切都是纯净的,它只有一种奇特的语法。
[ 编辑: 正如@Conal指出的,这实际上不是Haskell用于执行IO的。如果添加并发,或者在IO操作过程中世界发生任何变化,此模型都会中断,因此Haskell不可能使用此模型。它仅对顺序计算准确。因此,Haskell的IO可能有点躲闪。即使不是,也肯定不是那么优雅。根据@Conal的观察,请参见Simon Peyton-Jones在处理尴尬小队[pdf]的第3.1节中所说的内容;他根据这些思路提出了可能构成替代模型的内容,但由于其复杂性而放弃了它,并采取了不同的策略。]
同样,这(很大程度上)解释了IO和总体可变性在Haskell中的工作方式。如果这是你想知道的,你可以停止阅读这里。如果您想了解最后一门理论,请继续阅读-但请记住,这一点上,我们离您的问题还很遥远!
因此,最后一件事:事实证明,这种结构(带有return
和的参数类型>>=
)非常笼统;它被称为monad,do
符号return
和>>=
可以与其中任何一个一起使用。正如您在此处看到的那样,单子并不是魔幻的。神奇的是,do
块变成了函数调用。该RealWorld
类型是我们看不到任何魔法的唯一地方。[]
列表构造函数之类的类型也是monad,它们与不纯代码无关。
您现在(几乎)了解有关单子概念的所有信息(除了必须满足的一些定律和正式的数学定义),但您缺乏直觉。在线上有大量荒谬的monad教程;我喜欢这个,但您可以选择。但是,这可能对您没有帮助;获得直觉的唯一真正方法是结合使用它们并在适当的时间阅读一些教程。
但是,您不需要那种直觉就可以了解IO。全面了解monad无疑是锦上添花,但是您现在就可以使用IO。在我向您展示了第一个main
功能之后,您可以使用它。您甚至可以将IO代码当作不纯的语言来对待!但是请记住,有一个潜在的功能表示:没有人作弊。
(PS:很抱歉,长度太长了。我走得有点远。)