1. 第十六章 • 彩蛋

1.1. 只是开始

虽然我们看似已经使用我们的 Lisp 做了很多工作,但它仍和成为一门完整的,具有强大生产力的语言有一定的距离。如果您尝试将其用于任何足够大的项目,那么您总会遇到许多问题以及您必须进行的改进。解决这些问题能让您创造的语言更加能够融入成熟编程语言的圈子。

以下是您可能会遇到的一些问题,还有这些问题的潜在解决方案以及其他一些有趣的改进建议。有些方案可能需要再写几百行代码,有些则可能需要您写几千行代码。而选择要解决哪些问题,取决于您。如果您喜欢您的语言,您可能会喜欢做这些项目其中的一些,或者全部。

1.2. 原生类型

目前我们的语言只封装了 C 语言中的原生 longchar* 类型。对于您想要做任何更加有用的计算的情况来说,这是非常有限的。更何况,我们对这些数据类型的操作也非常有限。理想情况下,我们的语言应该封装所有原生 C 数据类型,并允许操作它们的方法。其中一个最重要的补充是允许操作十进制数。为此,您应当封装 double 类型和相关运算。而且,随着数据类型的增多,我们需要确保运算符例如 +- 能够很好地在它们各自或者集合上起效。

对于希望使用其语言中的十进制和浮点数进行计算的人来说,添加对原生类型的支持应该是相对来说比较有趣的

1.3. 用户定义的类型

除了添加对原生类型的支持之外,最好让用户能够添加自己的新类型,就像我们在 C 中使用结构一样。用于执行此操作的语法或方法将取决于您。这是一个非常重要的部分,能使我们的语言可用于任何大小合理的项目当中去。

这个任务对于那些对如何开发语言有特定想法的人,以及对语言本身,希望其最终设计看起来像什么的人来说,这可能会很有趣。

1.4. 字面列表

有些 Lisps 使用方括号 [] 为评估值列表的列表提供字面表示法。这种语法糖用于例如 list 100 (+ 10 20) 300 的情况。相反的,它让你写[100 (+ 10 20) 300]。在某些情况下,这显然更好,但它确实耗尽了[]这个字符的功能,尽管它本来能够用在更加有趣的环境中。

对于希望尝试添加额外语法的人来说,这应该是一个简单的补充。

1.5. 操作系统交互

引导语言的一个重要部分是为其提供正确的打开,读取和写入文件的能力。这意味着封装所有 C 的功能,例如freadfwritefgetc等在 Lisp 中的等同物。这是一项相当直观的任务,但确实需要编写大量的封装函数。这就是为什么到目前为止我们还没有为我们的语言做过类似的工作。

在类似的说明中,让我们的语言能够有权限并且适当地进行系统的调用是很好的。我们应该让它能够更改目录,列出目录中的文件以及诸如此类的功能。这是一项简单的任务,但同样需要封装大量的 C 函数。这对于任何想要将语言当作真实情况下的脚本语言来说,是极为重要的。。

希望利用他们的语言进行简单的脚本编写任务和字符串操作的人可能对实现此项目感兴趣。

1.6. 宏

许多其他 Lisps 允许您编写类似于 (def x 100) 定义值100x上。在我们的 lisp 中,这会不起作用,因为它会尝试计算x在环境中的存储为x的任何值。在其他 Lisps 中,这些函数称为,当遇到它们时,它们会停止对其参数的计算,并对它们进行未计算的操作。它们让你编写看起来像普通函数调用的东西,但实际上做的是复杂而有趣的事情。

语言中如果有这些将会很有趣。它们使语言可以为某些工作赋予一些魔力。在许多情况下,这可以使语法更好或允许用户不需要太过于单调。

我喜欢我们的语言在没有宏的时候,处理defif的过程。但是如果你不喜欢它,也就是语言当前的工作方式,并希望它与传统的Lisp更相似,那么这可能是你有兴趣实现的东西。

1.7. 变量哈希表

在我们用我们的语言查找变量名的那一刻,我们只是对当前环境中的所有变量进行线性搜索。我们定义的变量越多,这就变得越来越低效。

更有效的方法是实现哈希表。此技术将变量名称转换为整数,并使用此函数将索引转换为已知大小的数组,以查找与此符号关联的值。这是编程中非常重要的数据结构,并且由于其在重负载下的出色性能而无处不在。

任何有兴趣了解更多有关数据结构和算法的人都会很聪明地尝试实现这种数据结构或其中一种变体。

1.8. 池分配

我们的 Lisp 很简单,但速度不快。它的性能与某些脚本语言(如Python和Ruby)相似。我们程序中的大多数性能开销来自这样一个问题:几乎任何过程都需要我们构造和破坏lval。因此,我们必须经常调用malloc。这是一个很慢的函数,因为它需要操作系统为我们做一些管理。在进行计算时,会有很多lval类型的复制,分配和释放。

如果我们希望减少这种开销,我们需要降低malloc的调用次数。执行此操作的一种方法是让在程序开始时就调用一次malloc,分配大量内存。然后我们应该调用一些函数来替换我们所有的malloc调用,这些函数分割并分配这个内存以便在程序中使用。这意味着我们正在模拟操作系统的一些行为,但是只是以更快的本地方式进行。这种想法称为内存池分配,是游戏开发和其他对性能特别重视的应用程序中常用的技术。

这可能很难正确实现,但在概念上并不会太复杂。如果您想要一种快速获得性能提升的方法,那么您可能会对此感兴趣。

1.9. 垃圾回收

几乎所有其他 Lisps 实现都为我们的变量分配不同的变量。它们不会在环境中存储值的副本,而是实际上直接指向它的指针或引用。因为使用指针而不是副本,就像在 C 中一样,使用大型数据结构时所需的开销要少得多。

如果我们存储指向值而不是副本的指针,我们需要确保在某些其他值尝试使用之前,指向的数据不会被删除。我们希望在不再引用它时删除它。执行此操作的一种方法称为Mark和Sweep,用于监视环境中的值以及已分配的每个值。当一个变量被放入环境中时,它和它引用的所有内容都会被标记出来。然后,当我们希望释放内存时,我们可以迭代每个分配,并删除任何未标记的内容。

这称为垃圾回收,是许多编程语言不可或缺的一部分。与池分配一样,实现垃圾回收器不需要很复杂,但确实需要仔细完成,特别是如果您希望使其高效。实现这一点对于使这种语言适用于处理大量数据至关重要。您可以在此处找到有关在 C 中实现垃圾回收器的特别好的教程。

这应引起任何关注语言性能并希望改变语言中存储和修改变量的语义的人的兴趣。

1.10. 尾调用优化

我们的编程语言使用递归来进行循环。虽然这在概念上是一种非常聪明的方法,但实际上它很差。递归函数调用自身来收集计算的所有部分结果,然后才将所有结果组合在一起。当部分结果可以累积在一个循环中时,这是一种浪费的计算方法。对于旨在运行许多或无限迭代的循环而言,这尤其成问题。

一些递归函数可以自动转换为相应的while循环,这些循环逐步累积总数,而不是完全累积。这种自动转换称为尾调用优化,对于使用递归进行大量循环的程序是必不可少的优化。

对编译器优化和不同计算形式之间的对应关系感兴趣的人可能会觉得这个项目很有趣。

1.11. 词法作用域

当我们的语言查找到未定义的变量时,它会抛出错误。如果能在评估程序之前告诉我们哪些变量未定义,那应当会更好。而且,这将让我们避免拼写错误和其他烦人的错误。在程序运行之前查找这些问题称为词法作用域,并使用变量定义的规则来尝试和推断哪些变量已定义,哪些完全没在程序中用上,从而不进行任何计算。

这可能是一项难以完成的任务,但对于任何想要使编程语言更安全且不易出错的人来说,这应该是有趣的。

1.12. 静态类型

我们程序中的每个值都要有一个相关的类型。我们在进行任何计算之前都必须确保这一点。我们的内置函数也只将某些确切的类型作为输入。我们应该能够使用此信息来推断新用户定义的函数和值的类型。在运行程序之前,我们还可以使用此信息检查是否用户使用了正确的类型调用函数。这将减少在计算之前调用具有不正确类型的函数所产生的任何错误。此检查称为静态类型

类型系统是计算机科学中非常有趣和基本的一部分。它们是我们在运行程序之前检测错误的最佳方法。任何对编程语言安全和类型系统感兴趣的人都会发现这个项目非常有趣。

1.13. 结论

非常感谢您阅读本书。我希望您在它的页面上找到了一些有趣的东西。如果您喜欢它,请告诉您的朋友。如果您要继续开发您的语言,那么祝您好运,我希望您能学到更多关于 C 语言,编程语言和计算机科学的知识。

最重要的是,我希望您有乐趣构建自己的 Lisp 。不管何时!

results matching ""

    No results matching ""