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

时间:2022-04-18 16:14:52

So I'm currently working on a new programming language. Inspired by ideas from concurrent programming and Haskell, one of the primary goals of the language is management of side effects. More or less, each module will be required to specify which side effects it allows. So, if I were making a game, the graphics module would have no ability to do IO. The input module would have no ability to draw to the screen. The AI module would be required to be totally pure. Scripts and plugins for the game would have access to a very restricted subset of IO for reading configuration files. Et cetera.

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

However, what constitutes a side effect isn't clear cut. I'm looking for any thoughts or suggestions on the subject that I might want to consider in my language. Here are my current thoughts.

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

Some side effects are blatant. Whether its printing to the user's console or launching your missiles, anything action that reads or write to a user-owned file or interacts with external hardware is a side effect.

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

Others are more subtle and these are the ones I'm really interested in. These would be things like getting a random number, getting the system time, sleeping a thread, implementing software transactional memory, or even something very fundamental such as allocating memory.

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

Unlike other languages built to control side effects (looking at you Haskell), I want to design my language to be pragmatic and practical. The restrictions on side effects should serve two purposes:

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

  • To aid in the separations of concerns. (No one module can do everything).
  • 帮助分离关注点。 (没有一个模块可以做任何事情)。

  • To sandbox each module in the application. (Any module could be used as a plugin)
  • 沙箱应用程序中的每个模块。 (任何模块都可以用作插件)

With that in mind, how should I handle "pseudo"-side effects, like random numbers and sleeping, as I mention above? What else might I have missed? In what ways might I manage memory usage and time as resources?

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

4 个解决方案

#1


The problem of how to describe and control effects is currently occupying some of the best scientific minds in programming languages, including people like Greg Morrisett of Harvard University. To my knowledge, the most ambitious pioneering work in this area was done by David Gifford and Pierre Jouvelot in the FX programming language started in 1987. The language definition is online, but you may get more insight into the ideas by reading their 1991 POPL paper.

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

#2


This is a really interesting question, and it represents one of the stages I've gone through and, frankly, moved beyond.

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

I remember seminars in which Carl Hewitt, in talking about his Actors formalism, discussed this. He defined it in terms of a method giving a response that was solely a function of its arguments, or that could give different answers at different times.

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

I say I moved beyond this because it makes the language itself (or the computational model) the main subject, as opposed to the problem(s) it is supposed to solve. It is based on the idea that the language should have a formal underlying model so that its properties are easy to verify. That is fine, but still remains a distant goal, because there is still no language (to my knowledge) in which the correctness of something as simple as bubble sort is easy to prove, let alone more complex systems.

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

The above is a fine goal, but the direction I went was to look at information systems in terms of information theory. Specifically, assuming a system starts with a corpus of requirements (on paper or in somebody's head), those requirements can be transmitted to a program-writing machine (whether automatic or human) to generate source code for a working implementation. THEN, as changes occur to the requirements, the changes are processed through as delta changes to the implementation source code.

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

Then the question is: What properties of the source code (and the language it is encoded in) facilitate this process? Clearly it depends on the type of problem being solved, what kinds of information go in and out (and when), how long the information has to be retained, and what kind of processing needs to be done on it. From this one can determine the formal level of the language needed for that problem.

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

I realized the process of cranking through delta changes of requirements to source code is made easier as the format of the code comes more to resemble the requirements, and there is a nice quantitative way to measure this resemblence, not in terms of superficial resemblence, but in terms of editing actions. The well-known technology that best expresses this is domain specific languages (DSL). So I came to realize that what I look for most in a general-purpose language is the ability to create special-purpose languages.

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

Depending on the application, such special-purpose languages may or may not need specific formal features like functional notation, side-effect control, paralellism, etc. In fact, there are many ways to make a special-purpose language, from parsing, interpreting, compiling, down to just macros in an existing language, down to simply defining classes, variables, and methods in an existing language. As soon as you declare a variable or subroutine you're created new vocabulary and thus, a new language in which to solve your problem. In fact, in this broad sense, I don't think you can solve any programming problem without being, at some level, a language designer.

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

So best of luck, and I hope it opens up new vistas for you.

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

#3


A side effect is having any effect on anything in the world other than returning a value, i.e. mutating something that could be visible in some way outside the function.

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

A pure function neither depends on or affects any mutable state outside the scope of that invocation of the function, which means that the function's output depends only on constants and its inputs. This implies that if you call a function twice with the same arguments, you are guaranteed to get the same result both times, regardless of how the function is written.

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

If you have a function that modifies a variable that it has been passed, that modification is a side effect because it's visible output from the function other than the return value. A void function that is not a no-op must have side effects, because it has no other way of affecting the world.

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

The function could have a private variable only visible to that function that it reads and modifies, and calling it would still have the side effect of changing the way the function behaves in the future. Being pure means having exactly one channel for output of any kind: the return value.

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

It is possible to generate random numbers purely, but you have to pass around the random seed manually. Most random functions keep a private seed value that is updated each time its called so that you get a different random each time. Here's a Haskell snippet using System.Random:

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

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

The random functions each return the randomized value and a new generator with a new seed (based on the previous one). To get a new value each time, the chain of new generators (gen1,gen2,gen3) have to be passed along. Implicit generators just use an internal variable to store the gen1.. values in the background.

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

Doing this manually is a pain, and in Haskell you can use a state monad to make it a lot easier. You'll want to implement something less pure or use a facility like monads, arrows or uniqueness values to abstract it away.

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

Getting the system time is impure because the time could be different each time you ask.

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

Sleeping is fuzzier because sleep doesn't affect the result of the function, and you could always delay execution with a busy loop, and that wouldn't affect purity. The thing is that sleeping is done for the sake of something else, which IS a side effect.

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

Memory allocation in pure languages has to happen implicitly, because explicitly allocating and freeing memory are side effects if you can do any kind of pointer comparisons. Otherwise, creating two new objects with the same parameters would still produce different values because they would have different identities (e.g. not be equal by Java's == operator).

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

I know I've rambled on a bit, but hopefully that explains what side effects are.

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

#4


Give a serious look to Clojure, and their use of software transactional memory, agents, and atoms to keep side effects under control.

仔细研究Clojure,以及他们使用软件事务记忆,代理和原子来控制副作用。

#1


The problem of how to describe and control effects is currently occupying some of the best scientific minds in programming languages, including people like Greg Morrisett of Harvard University. To my knowledge, the most ambitious pioneering work in this area was done by David Gifford and Pierre Jouvelot in the FX programming language started in 1987. The language definition is online, but you may get more insight into the ideas by reading their 1991 POPL paper.

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

#2


This is a really interesting question, and it represents one of the stages I've gone through and, frankly, moved beyond.

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

I remember seminars in which Carl Hewitt, in talking about his Actors formalism, discussed this. He defined it in terms of a method giving a response that was solely a function of its arguments, or that could give different answers at different times.

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

I say I moved beyond this because it makes the language itself (or the computational model) the main subject, as opposed to the problem(s) it is supposed to solve. It is based on the idea that the language should have a formal underlying model so that its properties are easy to verify. That is fine, but still remains a distant goal, because there is still no language (to my knowledge) in which the correctness of something as simple as bubble sort is easy to prove, let alone more complex systems.

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

The above is a fine goal, but the direction I went was to look at information systems in terms of information theory. Specifically, assuming a system starts with a corpus of requirements (on paper or in somebody's head), those requirements can be transmitted to a program-writing machine (whether automatic or human) to generate source code for a working implementation. THEN, as changes occur to the requirements, the changes are processed through as delta changes to the implementation source code.

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

Then the question is: What properties of the source code (and the language it is encoded in) facilitate this process? Clearly it depends on the type of problem being solved, what kinds of information go in and out (and when), how long the information has to be retained, and what kind of processing needs to be done on it. From this one can determine the formal level of the language needed for that problem.

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

I realized the process of cranking through delta changes of requirements to source code is made easier as the format of the code comes more to resemble the requirements, and there is a nice quantitative way to measure this resemblence, not in terms of superficial resemblence, but in terms of editing actions. The well-known technology that best expresses this is domain specific languages (DSL). So I came to realize that what I look for most in a general-purpose language is the ability to create special-purpose languages.

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

Depending on the application, such special-purpose languages may or may not need specific formal features like functional notation, side-effect control, paralellism, etc. In fact, there are many ways to make a special-purpose language, from parsing, interpreting, compiling, down to just macros in an existing language, down to simply defining classes, variables, and methods in an existing language. As soon as you declare a variable or subroutine you're created new vocabulary and thus, a new language in which to solve your problem. In fact, in this broad sense, I don't think you can solve any programming problem without being, at some level, a language designer.

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

So best of luck, and I hope it opens up new vistas for you.

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

#3


A side effect is having any effect on anything in the world other than returning a value, i.e. mutating something that could be visible in some way outside the function.

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

A pure function neither depends on or affects any mutable state outside the scope of that invocation of the function, which means that the function's output depends only on constants and its inputs. This implies that if you call a function twice with the same arguments, you are guaranteed to get the same result both times, regardless of how the function is written.

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

If you have a function that modifies a variable that it has been passed, that modification is a side effect because it's visible output from the function other than the return value. A void function that is not a no-op must have side effects, because it has no other way of affecting the world.

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

The function could have a private variable only visible to that function that it reads and modifies, and calling it would still have the side effect of changing the way the function behaves in the future. Being pure means having exactly one channel for output of any kind: the return value.

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

It is possible to generate random numbers purely, but you have to pass around the random seed manually. Most random functions keep a private seed value that is updated each time its called so that you get a different random each time. Here's a Haskell snippet using System.Random:

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

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

The random functions each return the randomized value and a new generator with a new seed (based on the previous one). To get a new value each time, the chain of new generators (gen1,gen2,gen3) have to be passed along. Implicit generators just use an internal variable to store the gen1.. values in the background.

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

Doing this manually is a pain, and in Haskell you can use a state monad to make it a lot easier. You'll want to implement something less pure or use a facility like monads, arrows or uniqueness values to abstract it away.

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

Getting the system time is impure because the time could be different each time you ask.

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

Sleeping is fuzzier because sleep doesn't affect the result of the function, and you could always delay execution with a busy loop, and that wouldn't affect purity. The thing is that sleeping is done for the sake of something else, which IS a side effect.

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

Memory allocation in pure languages has to happen implicitly, because explicitly allocating and freeing memory are side effects if you can do any kind of pointer comparisons. Otherwise, creating two new objects with the same parameters would still produce different values because they would have different identities (e.g. not be equal by Java's == operator).

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

I know I've rambled on a bit, but hopefully that explains what side effects are.

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

#4


Give a serious look to Clojure, and their use of software transactional memory, agents, and atoms to keep side effects under control.

仔细研究Clojure,以及他们使用软件事务记忆,代理和原子来控制副作用。