要将IO函数添加到用Haskell编写的编程语言解释器中,我基本上有两种选择:
- 修改整个解释器,使其在IO单子内运行
- 允许被解释程序调用的运行时函数使用
unsafePerformIO
。
前者对我来说是一个坏主意——这有效地否定了任何纯度的好处,因为IO
几乎到达程序中的每个地方。我目前也大量使用ST
,并且必须修改大量的程序来实现这一点,因为我无法看到同时使用ST
和IO
(?)。
后者让我感到紧张——正如函数名所述,它是不安全的,但我认为在这种情况下它可能是合理的。特别是:
- 此更改所涉及的代码量非常小。
- 可以执行IO的点已经通过在解释表达式求值期间的控制点上使用
seq
显式地排序了。 - 也许更重要的是,IO操作返回的值只会在代码的解释部分中使用,在这里我可以保证引用透明度,因为解释器不能用相同的参数多次调用,作为操作计数器将作为相同更改的一部分贯穿整个系统,并且总是传递一个唯一的值给每个使用
unsafePerformIO
的函数。
在这种情况下,是否有很好的理由不使用unsafePerformIO
?
有人问我为什么要在解释器中保留纯洁性。原因有很多,但也许最紧迫的是,我打算稍后为这种语言构建一个编译器,该语言将包括各种元编程技术,这些技术将要求编译器包含解释器,但我希望能够保证编译结果的纯洁性。该语言将有一个纯子集用于此目的,我希望解释器在执行该子集时是纯的。
如果我理解正确,您希望将IO
动作添加到解释语言(不纯primops),而解释器本身是纯的。
第一个选项是abstract primops from interpreter。例如,解释器可以在一些未指定的单子中运行,而priops被注入:
data Primops m = Primops
{ putChar :: Char -> m ()
, getChar :: m Char
, ...
}
interpret :: Monad m => Primops m -> Program -> m ()
现在解释器不能执行任何IO
操作,除了primops的闭列表。(您可以使用自定义monad来获得类似的结果,而不是将primops作为参数传递。)
但我会认为这是过度工程,直到你确切地说为什么你需要纯解释器。也许你不知道?如果您只想使解释器的纯部分易于测试,那么最好将这些部分提取到单独的纯函数中。这样顶层的入口点将是不纯的,但是很小,但是所有解释器的逻辑都是可测试的。