State、ST、IORef和MVar之间的差异



我正在完成48小时内编写自己的方案(我最多可达85小时),并且我已经达到了关于添加变量和赋值的部分。本章有一个很大的概念跳跃,我希望它分两步完成,中间有一个好的重构,而不是直接跳到最终的解决方案。无论如何…

我已经迷失了许多不同的类,似乎服务于相同的目的:State, ST, IORefMVar。前三个在文中提到,而最后一个似乎是许多关于前三个的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计算运行在具有线程本地存储的单独线程中。

其他人已经完成了核心的事情,但是要回答直接的问题:

所有这些使得行类型为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问题",可以找到很多有趣的史前史。

相关内容

  • 没有找到相关文章

最新更新