我应该如何管理新语言设计中的副作用?
所以我目前正在研究一种新的编程语言。 受到并发编程和 Haskell 思想的启发,该语言的主要目标之一是管理副作用。 或多或少,每个模块都需要指定它允许哪些副作用。 所以,如果我正在制作游戏,图形模块将没有能力进行 IO。 输入模块将无法绘制到屏幕上。 人工智能模块需要完全纯净。 游戏的脚本和插件可以访问非常有限的 IO 子集来读取配置文件。 等等。
然而,什么构成副作用尚不清楚。 我正在寻找关于该主题的任何想法或建议,我可能想用我的语言来考虑。 这是我目前的想法。
有些副作用是显而易见的。 无论是打印到用户的控制台还是发射导弹,任何读取或写入用户拥有的文件或与外部硬件交互的操作都是副作用。
其他的则更微妙,这些是我真正感兴趣的。这些是诸如获取随机数、获取系统时间、休眠线程、实现软件事务内存,甚至是一些非常基本的东西(例如分配内存)。
与其他旨在控制副作用的语言(看看你的 Haskell)不同,我想将我的语言设计得务实且实用。 对副作用的限制应该有两个目的:
- 帮助分离关注点。 (没有一个模块可以做所有事情)。
- 将应用程序中的每个模块沙箱化。 (任何模块都可以用作插件)
考虑到这一点,我应该如何处理“伪”副作用,例如随机数和睡眠,正如我上面提到的? 我还可能错过了什么? 我可以通过哪些方式将内存使用和时间作为资源进行管理?
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.
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:
- 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?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
如何描述和控制效果的问题目前正在困扰着编程语言中一些最优秀的科学头脑,包括像 哈佛大学的格雷格·莫里塞特。 据我所知,该领域最雄心勃勃的开创性工作是由 David Gifford 和 Pierre Jouvelot 在 1987 年开始使用 FX 编程语言完成的。 语言定义是在线的,但是您可以通过阅读他们的 1991 POPL 论文。
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.
这是一个非常有趣的问题,它代表了我经历过的阶段之一,坦率地说,已经超越了。
我记得卡尔·休伊特在研讨会上谈论他的演员形式主义时讨论了这一点。 他将其定义为一种给出响应的方法,该响应仅是其参数的函数,或者可以在不同时间给出不同的答案。
我说我超越了这一点,因为它使语言本身(或计算模型)成为主要主题,而不是它应该解决的问题。 它基于这样的想法:语言应该有一个正式的底层模型,以便其属性易于验证。 这很好,但仍然是一个遥远的目标,因为(据我所知)仍然没有一种语言可以证明像冒泡排序这样简单的事情的正确性容易,更不用说更复杂的系统了。
以上是一个很好的目标,但我的方向是从信息论的角度来看待信息系统。 具体来说,假设一个系统以一个需求集(在纸上或在某人的头脑中)开始,这些需求可以被传输到程序编写机器(无论是自动的还是人类的)以生成用于工作实现的源代码。 然后,当需求发生更改时,这些更改将作为对实现源代码的增量更改进行处理。
那么问题是:源代码(及其编码语言)的哪些属性有助于此过程? 显然,这取决于要解决的问题的类型、输入和输出的信息类型(以及何时)、信息必须保留多长时间以及需要进行什么样的处理它。 由此可以确定该问题所需的语言的形式水平。
我意识到,通过对源代码的需求进行增量更改的过程变得更加容易,因为代码的格式与需求更加相似,并且有一种很好的定量方法来衡量这种相似性,而不是就表面相似而言,但就编辑动作而言。 最能表达这一点的众所周知的技术是领域特定语言(DSL)。 所以我开始意识到,我在通用语言中最需要的是创建专用语言的能力。
根据应用的不同,这种专用语言可能需要也可能不需要特定的形式特征,如函数符号、副作用控制、并行性等。事实上,有很多方法可以制作专用语言,从解析、解释、编译,直至现有语言中的宏,直至现有语言中的简单定义类、变量和方法。 一旦你声明了一个变量或子例程,你就创建了新的词汇,从而创建了一种用来解决你的问题的新语言。 事实上,从广义上讲,我认为如果不成为某种程度的语言设计师,就无法解决任何编程问题。
祝你好运,我希望它能为你打开新的视野。
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.
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.
副作用是对除了返回值之外的世界上的任何事物产生任何影响,即改变在函数之外以某种方式可见的东西。
纯函数既不依赖也不影响函数调用范围之外的任何可变状态,这意味着函数的输出仅依赖于常量及其输入。 这意味着,如果您使用相同的参数调用一个函数两次,则无论该函数是如何编写的,都保证两次都会得到相同的结果。
如果您有一个函数修改已传递的变量,则该修改是副作用,因为它是函数的可见输出,而不是返回值。 不是空操作的 void 函数必定有副作用,因为它没有其他方式影响世界。
该函数可以有一个私有变量,仅对其读取和修改的该函数可见,并且调用它仍然会产生改变该函数将来行为方式的副作用。 纯粹意味着只有一个通道用于任何类型的输出:返回值。
纯粹生成随机数是可能的,但您必须手动传递随机种子。 大多数随机函数都会保留一个私有种子值,该值在每次调用时都会更新,以便您每次都会获得不同的随机数。 这是使用 System.Random 的 Haskell 代码片段:
随机函数各自返回随机值和带有新种子的新生成器(基于前一个种子)。 为了每次获得新值,必须传递新生成器链(gen1、gen2、gen3)。 隐式生成器仅使用内部变量在后台存储 gen1.. 值。
手动执行此操作很痛苦,在 Haskell 中,您可以使用状态 monad 使其变得更容易。 您需要实现一些不太纯粹的东西,或者使用单子、箭头或唯一性值等工具来将其抽象出来。
获取系统时间是不纯粹的,因为每次询问的时间都可能不同。
睡眠更加模糊,因为睡眠不会影响函数的结果,并且您始终可以用繁忙的循环来延迟执行,这不会影响纯度。 问题是睡觉是为了其他事情而做的,这是一种副作用。
纯语言中的内存分配必须隐式发生,因为如果可以进行任何类型的指针比较,显式分配和释放内存都会产生副作用。 否则,创建具有相同参数的两个新对象仍会产生不同的值,因为它们具有不同的标识(例如,通过 Java 的 == 运算符不等于)。
我知道我已经胡言乱语了一些,但希望这能解释什么是副作用。
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:
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.
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.
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).
I know I've rambled on a bit, but hopefully that explains what side effects are.
认真研究 Clojure 以及他们对 软件事务内存、代理和原子,以控制副作用。
Give a serious look to Clojure, and their use of software transactional memory, agents, and atoms to keep side effects under control.