我正在完成48小时内编写自己的方案(我最多可达85小时),并且我已经达到了关于添加变量和赋值的部分。本章有一个很大的概念跳跃,我希望它分两步完成,中间有一个好的重构,而不是直接跳到最终的解决方案。无论如何…
我已经迷失了许多不同的类,似乎服务于相同的目的:State
, ST
, IORef
和MVar
。前三个在文中提到,而最后一个似乎是许多关于前三个的StackOverflow问题的最受欢迎的答案。在连续调用之间,它们似乎都带有一种状态。
这些都是什么,它们彼此之间有什么不同?
特别是这些句子没有意义:
相反,我们使用一个叫做状态线程的特性,让Haskell为我们管理聚合状态。这使得我们可以像对待其他编程语言一样对待可变变量,使用函数来获取或设置变量。
和
IORef模块允许您在IO单子中使用状态变量。
所有这些使得type ENV = IORef [(String, IORef LispVal)]
行令人困惑-为什么第二个IORef
?如果我写type ENV = State [(String, LispVal)]
,会发生什么?
State Monad:一个可变状态的模型
State单子是带有状态的程序的纯函数环境,有一个简单的API:
- 得到
- 把
mtl包中的文档。
当需要单个控制线程中的状态时,通常使用State单子。它在实现中并没有使用可变状态。相反,程序是由状态值参数化的(即状态是所有计算的附加参数)。状态只在单个线程中发生变化(并且不能在线程之间共享)。ST单子和STRefs
ST单子是IO单子的受限表亲。
它允许任意可变状态,作为机器上的实际可变内存实现。在没有副作用的程序中,API是安全的,因为rank-2类型参数防止依赖于可变状态的值脱离局部作用域。
它允许在其他纯粹的程序中控制可变性。
通常用于可变数组和其他数据结构的突变,然后冻结。这也是非常有效的,因为可变状态是"硬件加速的"。
主API:- Control.Monad.ST
- runST——开始一个新的内存效果计算
- 和STRefs:指向(本地)可变单元格的指针。
- 基于st的数组(如vector)也很常见。
可以把它看作是IO单子的不那么危险的兄弟。或者IO,只能对内存进行读写。
IORef: STRefs in IO
这些是IO单子中的stref(见上文)。它们没有与STRefs相同的关于局部性的安全保证。
MVars: IORefs with locks
类似于STRefs或IORefs,但是附加了一个锁,用于从多个线程中安全的并发访问。ioref和stref只有在使用atomicModifyIORef
(比较-交换原子操作)时才安全。mvar是一种更通用的安全共享可变状态的机制。
通常,在Haskell中,使用mvar或tvar(基于stm的可变单元格),而不是STRef或IORef。
好的,我将从IORef
开始。IORef
提供了一个在IO单子中可变的值。它只是对一些数据的引用,像任何引用一样,有一些函数允许你改变它所引用的数据。在Haskell中,所有这些函数都在IO
中操作。您可以把它想象成数据库、文件或其他外部数据存储——您可以在其中获取和设置数据,但是这样做需要通过IO。IO之所以必要,是因为Haskell是纯的;编译器需要一种方法来知道引用在任何给定时间指向哪个数据(阅读sigfpe的"你可能已经发明了monads"blogpost)。
MVar
s基本上与IORef相同,除了两个非常重要的区别。MVar
是一个并发原语,所以它是为多线程访问而设计的。第二个区别是MVar
是一个可以是满的或空的盒子。因此,当IORef Int
总是有Int
(或其底部)时,MVar Int
可能有Int
,也可能为空。如果一个线程试图从一个空的MVar
中读取一个值,它将阻塞,直到MVar
被填满(由另一个线程)。基本上,MVar a
等同于IORef (Maybe a)
,只是额外的语义对并发性很有用。
State
是一个monad,它提供可变状态,不一定是IO。事实上,它对纯计算特别有用。如果你有一个使用state而不是IO
的算法,那么State
monad通常是一个很好的解决方案。
State还有一个单子转换器版本,StateT
。这通常用于保存程序配置数据,或者应用程序中的"游戏-世界-状态"类型的状态。
ST
略有不同。ST
中的主要数据结构是STRef
,它类似于IORef
,但具有不同的monad。ST
monad使用类型系统技巧(文档中提到的"状态线程")来确保可变数据不能逃离monad;也就是说,当您运行ST计算时,您会得到一个纯结果。ST之所以有趣,是因为它是一个像IO一样的原始单子,允许计算对字节数组和指针执行低级操作。这意味着ST
可以在对可变数据使用低级操作的同时提供一个纯接口,这意味着它非常快。从程序的角度来看,似乎ST
计算运行在具有线程本地存储的单独线程中。
其他人已经完成了核心的事情,但是要回答直接的问题:
Lisp是一种具有可变状态和词法作用域的函数式语言。假设你封闭了一个可变变量。现在,您已经在其他函数中获得了对该变量的引用——比如(用haskell风格的伪代码)所有这些使得行类型为
ENV = IORef [(String, IORef LispVal)]
让人困惑。为什么是第二个IORef?什么如果我做type ENV = State [(String, LispVal)]
,会打破吗?
(printIt, setIt) = let x = 5 in ( () -> print x, y -> set x y)
。现在有两个函数——一个打印x,一个设置它的值。当计算printIt
时,您希望在定义printIt
的初始环境中查找x的名称,但是您希望在printIt
被调用的环境中查找该名称绑定到的值(在setIt
可能被调用任意次数之后)。
除了这两个ioref之外,还有其他方法可以做到这一点,但您肯定需要的不仅仅是您建议的后一种类型,它不允许您以词法作用域的方式更改名称绑定到的值。在谷歌上搜索"funargs问题",可以找到很多有趣的史前史。