我一直在思考一种新语言背后的一些概念。起初它是一种玩具,但现在我想知道它是否真的意味着什么。我将这个问题发布到 Stack Overflow 以查看之前是否已完成,以及是否可以获得任何反馈、想法或其他信息。
我主要是在阅读了Jonathan Edward 关于声明式编程的演讲后开始思考这个问题的。然后我将它与我的一些旧想法以及我在现代语言中看到的东西混合在一起。
声明式编程背后的主要思想是“什么”与“如何”。然而,我已经听过很多次了,所以它似乎几乎总是像“有趣”这个词,它实际上并没有告诉你任何东西,这令人沮丧。
然而,在 Jonathan Edward 的版本中,他首先强调惰性求值。这会产生一些有趣的结果,即函数式反应式编程 (FRP)。这是带有动画的 FRP 示例(使用我编写的语法):
x as time * 2 // time is some value representing the current time
y as x + (2 * 500)
new Point(x, y)
因此,如果输入发生变化,这里的值会自动改变。在我最喜欢的语言之一D中,“纯”和“不纯”函数之间存在区别。纯函数是与外界没有任何联系,只使用其他纯函数的函数。不然就不纯了。关键是你总是可以相信一个纯函数为给定的参数返回相同的值。
我想这里也适用类似的传递原则。我们的杂质是time
。time
被、 存在x
、 因此y
、 因此所触及的一切new Point(x, y)
都是不纯的。然而,通知(2 * 500)
是纯粹的。所以你看到这告诉编译器它的限制在哪里。我认为它就像用变量简化数学表达式:
(x ^ 2) + 3x + 5
(4 ^ 2) + 3x + 5 = 16 + 3x + 5 = 21 + 3x = 3(7 + x)
通过告诉编译器什么是纯的,什么不是,我们可以大大简化我们的程序。另一点是急切或可变的数据。乔纳森爱德华认为输入是可变的和渴望的,但输出是功能性的和懒惰的。基本上,给定新的输入,程序定义了一个原子状态变化,然后输出只是当前状态的一个函数。如果您想了解为什么这很重要,请参阅演示文稿。输入不纯。惰性求值有助于定义原子状态变化。让我们看一下程序是如何编写的:
void main ()
{
String input = "";
writeln("Hello, world!");
writeln("What's your name? ");
input = readln();
writeln("Hello, %s!", input);
writeln("What's your friends name? ");
input = readln();
writeln("Hello to you too, %s!", input);
}
这里的bind
关键字表示如果begin
发生更改,则执行以下代码。mutable
关键字表示输入不是懒惰的,而是急切的。现在让我们看看“原子状态变化”如何表示它。
program:
mutable step := 0
bind begin:
writeln("Hello, world!")
writeln("What's your name? ")
++step
bind readln() as input when step = 1:
writeln("Hello, %s!", input)
writeln("What's your friends name? ")
++step
bind readln() as input when step = 2:
writeln("Hello to you too, %s!", input)
现在,我们看到了一些对于程序员来说可以变得更容易、更易读的东西。首先是丑陋的step
变量,以及我们每次必须如何递增和测试它。下面是一个新的改进版本的示例:
program:
bind begin:
writeln("Hello, world!")
writeln("What's your name? ")
bind readln() as input:
writeln("Hello, %s!", input)
writeln("What's your friends name? ")
yield // This just means the program jumps to here instead of at the beginning
writeln("Hello to you too, %s!", input)
halt
这样更好。不过,并不完美。但如果我知道完美的答案,我就不会在这里了,对吧?
这是一个更好的例子,使用游戏引擎:
class VideoManager:
bind begin: // Basically a static constructor, will only be called once and at the beginning
// Some video set up stuff
bind end: // Basically a static destructor
// Some video shut down stuff
class Input:
quitEvent as handle // A handle is an empty value, but can be updated so code that's bound to it changes.
keyboardEvent as handle(KeyboardEvent) // This handle does return a value though
mouseEvent as handle(MouseEvent)
// Some other code manages actually updating the handles.
class Sprite:
mutable x := 0
mutable y := 0
bind this.videoManager.updateFrame:
// Draw this sprite
class FieldState:
input as new Input
player as new Sprite
bind input.quitEvent:
halt
bind input.keyboardEvent as e:
if e.type = LEFT:
this.player.x -= 2
else if e.type = RIGHT:
this.player.x += 2
else if e.type = UP:
this.player.y -= 2
else if e.type = DOWN:
this.player.y += 2
我喜欢这不需要回调、事件,甚至循环或任何东西,而且线程是显而易见的。更容易判断发生了什么,而且不仅仅是类似于 Python 的语法。我认为这就像语言开发人员意识到人们使用标签和 goto 的只有少数东西:条件分支和循环。因此,他们将 if-then-else、while 和 for 构建到语言中,标签和 goto 已被弃用,编译器和人们可以知道发生了什么。我们使用的大部分内容都来自该过程。
回到线程,这样做的好处是线程更加灵活。如果编译器可以自由地做它想做的事,因为我们已经更接近说出我们想要的东西,而不是我们想要它如何完成。因此,编译器可以利用多核和分布式处理器,但仍然可以补偿没有良好线程支持的平台。
我想提最后一件事。这就是我对模板的看法。这是一个概念性的鸡蛋,在我开始编程时开始发展(实际上是大约 2 年前),然后开始破解。基本上它是抽象的原则,但它比类和对象延伸得更远。
这与我对功能的看法有关。例如:
int add (int a, int b)
{
return a + b;
}
好的,add
返回一个int
,但它是什么?这有点像是在int
等待发生。就像一个没有几块的拼图。可能性有限,只有某些部分适合,但当你完成后,你就有了一个成品,你可以在其他地方使用。就像我说的,这就是抽象的原则。以下是一些我认为是抽象+缺失部分->具体关系的示例:
- 函数 + 参数 -> 值
- 抽象类+方法->类
- 类 + 实例值 -> 对象
- 模板 + 参数 -> 函数或类
- 程序 + 输入 + 状态 -> 输出
它们都是密切相关的。似乎可以利用这一点。但是怎么做?同样,这就是为什么这是一个问题。但是惰性求值在这里很有趣,因为您可以将仍然缺少部分的东西传递给其他东西。对于编译器来说,这主要是将名称取消引用到杂质的问题。就像我上面的例子:
(x ^ 2) + 3x + 5
(4 ^ 2) + 3x + 5 = 16 + 3x + 5 = 21 + 3x = 3(7 + x)
你给编译器的部分越多,它就越能完成它并将程序简化为它的基本核心。并且add
上面的函数将在编译时自动解析,因为它不依赖于外部资源。甚至可以解析大量的类和对象,以及大部分程序,这取决于编译器的智能程度。
目前为止就这样了。如果你已经看过这些事情的例子,我想看看。如果您有任何想法、创新、资源或反馈,我也将不胜感激。