如何在新的语言设计中管理副作用?

时间:2009-04-29 16:21:46

标签: programming-languages haskell side-effects

所以我正在研究一种新的编程语言。受并发编程和Haskell的启发,该语言的主要目标之一是管理副作用。或多或少,每个模块都需要指定它允许的副作用。所以,如果我正在制作游戏,那么图形模块将无法执行IO。输入模块无法绘制到屏幕上。 AI模块必须是完全纯净的。游戏的脚本和插件可以访问非常有限的IO子集来读取配置文件。等等。

然而,副作用的构成并不明确。我正在寻找有关我可能想用我的语言考虑的主题的任何想法或建议。这是我目前的想法。

有些副作用是明显的。无论是打印到用户控制台还是发射导弹,任何读取或写入用户自有文件或与外部硬件交互的操作都会产生副作用。

其他更微妙,这些是我真正感兴趣的。这些就像获取随机数,获取系统时间,睡眠线程,实现软件事务内存,甚至是非常基本的东西,如分配内存。

与其他用于控制副作用的语言(看着你的Haskell)不同,我想设计我的语言是务实和实用的。对副作用的限制应该有两个目的:

  • 帮助分离关注点。 (没有一个模块可以做任何事情)。
  • 对应用程序中的每个模块进行沙箱处理。 (任何模块都可以用作插件)

考虑到这一点,如上所述,我应该如何处理“伪”边效应,如随机数和睡眠?还有什么我可能错过的?我可以通过哪些方式将内存使用和时间作为资源进行管理?

4 个答案:

答案 0 :(得分:4)

如何描述和控制效果的问题目前占据了编程语言中一些最优秀的科学家,包括像哈佛大学Greg Morrisett这样的人。据我所知,这个领域最雄心勃勃的开创性工作是由David Gifford和Pierre Jouvelot在1987年开始的FX编程语言中完成的。language definition是在线的,但是你可以通过阅读他们来更深入地了解这些想法。 1991 POPL paper

答案 1 :(得分:2)

这是一个非常有趣的问题,它代表了我经历过的一个阶段,坦率地说,已经超越了。

我记得卡尔·休伊特在谈论他的演员形式主义时所讨论过的研讨会。他根据一种方法来定义它,该方法只给出了一个响应,它只是一个参数的函数,或者可以在不同的时间给出不同的答案。

我说我超越了这一点,因为它使语言本身(或计算模型)成为主要主题,而不是它应该解决的问题。它基于这样的想法:语言应该具有正式的底层模型,以便其属性易于验证。这很好,但仍然是一个遥远的目标,因为仍然没有语言(据我所知),其中像泡泡排序一样简单的东西的正确性容易来证明,更不用说更复杂的系统了

以上是一个很好的目标,但我的方向是根据信息理论来看待信息系统。具体来说,假设系统以需求语料库(在纸上或在某人的脑海中)开始,那些需求可以传输到程序编写机器(无论是自动还是人工)以生成工作实现的源代码。然后,当需求发生变化时,通过对实现源代码的delta更改来处理更改。

然后问题是:源代码的哪些属性(及其编码的语言)有助于此过程?显然,这取决于要解决的问题的类型,进出的信息类型(以及何时),信息需要保留多长时间以及需要在何种类型的处理上进行它。从这一点可以确定该问题所需语言的正式级别。

我意识到,由于代码的格式更多地符合类似于的要求,因此对源代码的需求增量变化的过程变得更加容易,并且有一种很好的定量方法来衡量这一点相似性,不是表面上的相似性,而是在编辑行为方面。最能表达这一点的众所周知的技术是领域特定语言(DSL)。所以我开始意识到,我在通用语言中最需要的是能够创建专用语言。

根据应用程序的不同,这些特殊目的语言可能需要也可能不需要特定的形式功能,如功能表示法,副作用控制,并列等等。实际上,有许多方法可以制作一种特殊目的语言,从解析,解释,编译,简化为现有语言中的宏,直到简单地定义现有语言中的类,变量和方法。一旦声明了变量或子程序,就会创建新的词汇表,从而创建一种新的语言来解决您的问题。事实上,从广义上讲,我不认为你可以解决任何编程问题没有在某种程度上是语言设计师。

祝你好运,我希望它为你开辟新的前景。

答案 2 :(得分:1)

副作用是对世界上的任何事物产生任何影响,而不是返回一个值,即改变在函数之外以某种方式可见的东西。

纯函数既不依赖于也不影响函数调用范围之外的任何可变状态,这意味着函数的输出仅取决于常量及其输入。这意味着如果使用相同的参数调用函数两次,则无论函数是如何编写的,都可以保证两次都得到相同的结果。

如果你有一个函数来修改它已经传递的变量,那么这个修改就是副作用,因为它是函数的可见输出而不是返回值。一个不是无操作的虚函数必须有副作用,因为它没有其他影响世界的方法。

该函数可以有一个私有变量,只对它读取和修改的函数可见,并且调用它仍然会产生改变函数未来行为方式的副作用。纯粹意味着只有一个输出通道:返回值。

可以纯粹生成随机数,但您必须手动传递随机种子。大多数随机函数都会保留一个私有种子值,每次调用它时都会更新,以便每次都获得不同的随机值。这是使用System.Random的Haskell片段:

randomColor              :: StdGen -> (Color, Int, StdGen)
randomColor gen1         = (color, intensity, gen2)
 where (color, gen2)     = random gen1
       (intensity, gen3) = randomR (1, 100) gen2

随机函数分别返回随机值和带有新种子的新生成器(基于前一个)。为了每次获得一个新值,必须传递新的生成器链(gen1,gen2,gen3)。隐式生成器只使用内部变量在背景中存储gen1 ..值。

手动执行此操作很麻烦,在Haskell中,您可以使用状态monad使其更容易。你需要实现一些不那么纯粹的东西,或者使用像monad,箭头或唯一性值这样的工具来抽象它。

获取系统时间是不纯的,因为每次询问时间可能会有所不同。

睡眠是模糊的,因为睡眠不会影响函数的结果,并且您总是可以使用繁忙的循环延迟执行,这不会影响纯度。问题是睡觉是为了别的事情而做的,这是副作用。

纯语言中的内存分配必须隐式发生,因为如果可以进行任何类型的指针比较,显式分配和释放内存都是副作用。否则,创建具有相同参数的两个新对象仍会产生不同的值,因为它们将具有不同的标识(例如,不等于Java的==运算符)。

我知道我有点絮絮叨叨,但希望这可以解释副作用是什么。

答案 3 :(得分:0)

认真审视Clojure,并使用software transactional memory, agents, and atoms来控制副作用。