6

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

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

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

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

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

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

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

4

4 回答 4

4

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

于 2009-04-29T23:15:05.143 回答
2

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

我记得卡尔·休伊特在讨论他的演员形式主义时讨论过这个问题。他将其定义为一种给出响应的方法,该响应仅是其参数的函数,或者可以在不同时间给出不同的答案。

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

以上是一个很好的目标,但我的方向是从信息论的角度来看待信息系统。具体来说,假设一个系统从一个需求语料库开始(在纸上或在某人的脑海中),这些需求可以传输到程序编写机器(无论是自动的还是人工的)以生成用于工作实现的源代码。然后,随着需求发生变化,这些变化将作为实现源代码的增量变化进行处理。

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

我意识到随着代码的格式越来越类似于需求,通过对源代码的需求的增量更改进行启动的过程变得更加容易,并且有一种很好的定量方法来衡量这种相似性,不是从表面上的相似性,而是在编辑操作方面。最能表达这一点的著名技术是领域特定语言 (DSL)。所以我开始意识到,我在通用语言中最看重的是创建专用语言的能力。

根据应用程序的不同,这种专用语言可能需要也可能不需要特定的形式特征,如函数符号、副作用控制、并行性等。实际上,制作专用语言有很多方法,从解析、解释, 编译,到现有语言中的宏,再到在现有语言中简单地定义类、变量和方法。一旦您声明了一个变量或子例程,您就会创建新的词汇表,从而创建一种新的语言来解决您的问题。事实上,从广义上讲,我认为如果在某种程度上不是语言设计师,你就无法解决任何编程问题。

祝你好运,我希望它为你开辟了新的视野。

于 2009-05-05T18:44:47.783 回答
1

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

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

如果您有一个函数修改已传递的变量,则该修改是副作用,因为它是函数的可见输出而不是返回值。一个非空操作的 void 函数必须有副作用,因为它没有其他影响世界的方式。

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

可以纯粹生成随机数,但您必须手动传递随机种子。大多数随机函数都会保留一个私有种子值,该值在每次调用时都会更新,以便每次获得不同的随机数。这是使用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 中,您可以使用 state monad 使其更容易。你会想要实现一些不那么纯粹的东西,或者使用像 monads、箭头或唯一性值这样的工具来抽象它。

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

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

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

我知道我有点啰嗦,但希望这能解释什么是副作用。

于 2009-04-29T17:47:10.237 回答
0

认真看看Clojure以及它们使用软件事务内存、代理和原子来控制副作用。

于 2009-04-29T16:24:47.843 回答