我正在通过Write Yourself a Scheme in 48 Hours(我大约85小时),我已经完成了关于Adding Variables and Assignments的部分。本章中有一个很大的概念性跳跃,我希望它分两步完成,两者之间有很好的重构,而不是直接跳到最终的解决方案。总之...
我已经迷失了许多看似用于同一目的的不同类:State
,ST
,IORef
和MVar
。文本中提到了前三个,而最后三个似乎是许多关于前三个问题的StackOverflow问题的最佳答案。它们似乎在连续调用之间都处于状态。
这些是什么以及它们如何彼此不同?
特别是这些句子没有意义:
相反,我们使用一个名为状态线程的功能,让Haskell为我们管理聚合状态。这使我们可以像处理任何其他编程语言一样处理可变变量,使用函数来获取或设置变量。
和
IORef模块允许您在IO monad 中使用有状态变量。
所有这些都会让行type ENV = IORef [(String, IORef LispVal)]
感到困惑 - 为什么第二个IORef
?如果我改写type ENV = State [(String, LispVal)]
会怎样?
答案 0 :(得分:111)
State Monad:一个可变状态的模型
状态monad是一个纯函数的环境,用于具有状态的程序,具有简单的API:
the mtl package中的文档。
状态monad通常在单个控制线程中需要状态时使用。它实际上并没有使用可变状态。相反,程序由状态值参数化(即状态是所有计算的附加参数)。状态似乎只在一个线程中发生变异(并且不能在线程之间共享)。
ST monad和STRefs
ST monad是IO monad的受限表兄。
它允许任意可变状态,在机器上实现为实际的可变内存。 API在无副作用的程序中是安全的,因为rank-2类型参数防止依赖于可变状态的值逃避本地范围。
因此,它允许在其他纯程序中实现受控的可变性。通常用于可变数组和其他变异的数据结构,然后被冻结。它也非常有效,因为可变状态是“硬件加速”。
主要API:
将其视为IO monad中较不危险的兄弟姐妹。或者IO,你只能读写内存。
IORef:IO中的STRefs
这些是IO monad中的STRef(见上文)。它们没有STRefs关于地点的安全保障。
MVars:带锁的IORef
与STRefs或IORefs类似,但附加了锁,以便从多个线程进行安全的并发访问。使用atomicModifyIORef
(比较和交换原子操作)时,IORef和STRef在多线程设置中是安全的。 MVars是一种更安全地共享可变状态的更通用的机制。
通常,在Haskell中,使用MVars或TVars(基于STM的可变单元格),而不是STRef或IORef。
答案 1 :(得分:36)
好的,我将从IORef
开始。 IORef
提供IO monad中可变的值。它只是对某些数据的引用,与任何引用一样,有些功能允许您更改它引用的数据。在Haskell中,所有这些函数都在IO
中运行。您可以将其视为数据库,文件或其他外部数据存储 - 您可以在其中获取和设置数据,但这样做需要通过IO。 IO是必要的原因是因为Haskell 纯;编译器需要一种方法来知道参考在任何给定时间指向哪些数据(阅读sigfpe's "You could have invented monads"博文)。
MVar
与IORef基本相同。 MVar
是一个并发原语,因此它设计用于从多个线程进行访问。第二个区别是MVar
是一个可以满或空的框。因此,IORef Int
始终具有Int
(或底部),MVar Int
可能有Int
或者可能为空。如果一个线程试图从空MVar
读取一个值,它将一直阻塞,直到MVar
被另一个线程填充。基本上,MVar a
等同于IORef (Maybe a)
,其具有对并发有用的额外语义。
State
是一个提供可变状态的monad,不一定是IO。实际上,它对纯计算特别有用。如果您的算法使用状态但不使用IO
,则State
monad通常是一种优雅的解决方案。
还有一个Monad变换器版本的状态StateT
。这经常被用于保存程序配置数据或应用程序中的“游戏世界状态”类型的状态。
ST
略有不同。 ST
中的主要数据结构是STRef
,它类似于IORef
但具有不同的monad。 ST
monad使用类型系统技巧(文档中提到的“状态线程”)来确保可变数据不能逃脱monad;也就是说,当你运行ST计算时,你会得到一个纯粹的结果。 ST很有趣的原因是它是一个像IO一样的原始monad,允许计算对字节数组和指针执行低级操作。这意味着ST
可以在对可变数据使用低级操作时提供纯接口,这意味着它非常快。从程序的角度来看,就好像ST
计算在一个带有线程本地存储的单独线程中运行。
答案 2 :(得分:18)
其他人已经做了核心事情,但要回答直接问题:
所有这些都使得线型
ENV = IORef [(String, IORef LispVal)]
混乱。为什么第二个IORef?什么 如果我改为type ENV = State [(String, LispVal)]
会破裂吗?
Lisp是一种具有可变状态和词法范围的函数式语言。想象一下,你已经关闭了一个可变变量。现在你已经在其他函数中引用了这个变量 - 比如说(在haskell风格的伪代码中)(printIt, setIt) = let x = 5 in (\ () -> print x, \y -> set x y)
。您现在有两个功能 - 一个打印x,一个设置其值。在评估printIt
时,您希望在定义printIt
的初始环境中查找x的名称,但是您要查找值该名称在printIt
被称为的环境中被绑定(setIt
可能被多次调用之后)。
有两种方法可以让两个IORef这样做,但你确实需要的不仅仅是你提出的后一种类型,它不允许你以词法范围的方式改变名称绑定的值。谷歌是一个有趣的史前史的“funargs问题”。