永垂不朽的编程语言(Lisp)的多面性

2020-07-21 12:50:07

这是我一段时间以来一直想写的帖子:一则关于比较Lisp家族编程语言的轶事。我认为自己是一个Lisp黑客。也许这一点从装饰在我的网站标题上的字母λ中可见一斑,这是对约翰·麦卡锡(John McCarthy)设计第一个LISP的灵感所在的λ微积分的引用。然而,除非你也认为自己是一个Lisp黑客,否则Lisp黑客可能没有什么意义。称自己为一个人似乎带有某种程度的不言而喻的含义。事实上,有些人认同更具体的群体。阴谋家、阴谋家、拉克泰尔、收藏家、⊇黑客、阴谋家等都属于更具体的群体,而不是里斯普黑客(LISP hacker";GULER";或";Racketeer,";或";CloJurist";LISP hacker";或";Guiler";或";Racketeer,";或";所有这些编程语言或至少大多数编程语言都有共同性,Lisp黑客认识到并理解这种共同性-使编程语言成为Lisp的特征。在我看来,无与伦比的同形语法、强大的元编程工具和编辑器支持。(是的,我指的是GNU Emacs。)。然而,这篇文章关注的是这些不同之处。在这篇文章中,我将考虑每种方言的特点,以及这些特点是否构成我想用来开发一款新软件的语言。

在写这篇文章的时候,我特别关注游戏开发。我突然有了一个以回合为基础的战术游戏的想法,我觉得Lisp将是实现它的最好工具,但使用Lisp的决定仍然给我留下了几个选择。当我列举每种方言背后值得注意的设计选择,并谈到我更喜欢的方法时,我的观点在某种程度上会被框定为对这样一个问题的部分回答:我能轻松地用它来写视频游戏吗?因此,有几件事我特别感兴趣:

人体工程学,或称“人体工程学”,是对人在试图完成任务时的摩擦力[体验]的一种衡量方法[2]。

性能,这不是适当量化的微不足道的[3]。我不会对此严格要求的;一次性地与时间一起运行可以很好地了解执行时间的大小。

易于分发,这很难定义,但我将其与平台不可知论联系在一起,运行时不会使我的tarball膨胀几个千兆字节,以及缺乏巴洛克风格和难以获得依赖。

能够与其他库交互,因为我希望能够绘制到屏幕上,并播放声音,等等。

对于每一种方言,我都允许自己使用非标准功能。我的目标是评估每种语言的实用方面,如果您正在编写软件,您可能会使用超过R5RS或ANSI CL标准中包含的内容。不过,如果这些非标准函数特定于单个实现,我会避免使用它们。SRFI和QuickLisp是公平的竞争对手,但鸡的鸡蛋不是。啊,我已经言过其实了。是的,我将比较Scheme和Common Lisp。我几乎不得不这么做--Lisp的历史往往被编织成Common Lisp和Scheme之间的分裂。我还会谈到其他几个人。我大多选择存在一些游戏引擎类型库的方言。对于R7RS(鸡肉),有超大的;对于R6RS(Guile),有山雀;对于Common Lisp,有Xelf;对于茴香,当然有L?VE。

以下是我的观点,所以我想先介绍一下激励他们的背景。我最早认真使用Lisp的经历是在PeterSeibel的“实用通用Lisp”上,这是我在高中尝试阅读计算机程序的结构和解释失败后学到的。1我所管理的后一本书的部分足以让我相信学习Lisp是有价值的,但是学习CommonLisp可能比学习方案更容易。大学一年级后的那个夏天,我自学了为GNU Guix做GSOC的计划。Guilequickly逐渐让我喜欢上了,我很快就开始在我的个人网站上使用“出没”了。我从很久以前就开始不知不觉地使用Emacs Lisp了--不是在编写包的意义上--我父亲在我9岁的时候教我如何使用Emacs,但我基本上不用编写setq表单。我也用过Hy,Fennel,…。嗯,我现在完全偏离正轨了。重点是,我已经使用了很多LISP,我下意识地承认了它们之间的差异,但从未将这种承认转化为连贯的思想。

为了便于比较,我用几种Lisp方言编写了相同的raytracer。我选择光线跟踪器的原因是:

这不是一件微不足道的事,但与高性能数据库的几个实现相比,raytracer的几个实现也更容易处理。

另一个要考虑的问题是,光线追踪的进步建立在相同的基础结构上,这可能会给我一种比较系统更改的简易性的方法,但编写这些光线跟踪器已经够我累的了,以至于我不想再玩它们了。(#**$$=“{laugh}{##**$$}{##**$$}{laugh}{##**$$}{laugh}{##**$$})。

这远不如我希望的那样能说明问题。一旦我完成了第一个光线跟踪器,接下来的一切都有相同的结构,不管怎样,编写这些光线跟踪器让我对我感兴趣的特性有了一个概念,特别是性能。对于任何想要查看代码的人,都可以在这里获得实现。

嗯,如果我们要写一个光线跟踪器,那么我们最好有一些方法来看看结果。问题在于可移植性。理想情况下,我希望能够在每种语言的不同实现上运行层程序,但它们都没有标准化的图形绘制支持。我的一个想法是使用ANSI转义序列将图像渲染到终端,但我认为生成的图像会非常糟糕。取而代之的是,我决定走tinyrenderer采取的路线,即输出到图像文件。最初,我使用的图像格式是令人尊敬的PNG。这是个错误。即使它确实在方案中导致了一个相当优雅的CRC过程。

(DEFINE(字节)(DEFINE(CRC字节)(BIT-XOR(Vector-REF PNG-CRC(BIT-AND#xFF(BIT-XOR CRC字节)(算术移位CRC-8)(Reduce Process-Byte Bytes#xFFFFFFF)。

意识到PNG是不必要的复杂,我接着编写了一个BMP编码器,这个编码器本来还不错,直到我看到Chris Wellons的一篇文章,关于用C语言通过将帧编码为Netpbm图像来呈现视频。我决定放弃我的BMP编码器,转而使用PPM。Netpbm是基于文本的:例如,Scheme中的PNG或BMP编码器的问题是,您正在处理的是二进制格式。现在浏览一下标准,似乎确实有标准化的过程来处理R6RS和R7RS中的二进制数据。无论如何,处理这些二元结构并不得不考虑字符顺序是徒劳的。Ppm非常简单。事实上,我敢打赌,如果你能访问的都是维基百科页面上的例子,你一定能写出一个编码器。此处介绍该计划的实施情况:

(DEFINE(宽度高度像素)";将以像素形式给出的宽度与高度图像编码成可移植像素映射格式(PPM),将结果写入`(当前输出端口)';";(DEFINE(VALUES)(cond((NULL?值)(换行符))((=1(长度值))(显示(车值))(分隔值(CDR值))(ELSE(显示(车值))(DISPLAY";";)(分隔值(CDR值);;Magic(分隔值';(";P3";);尺寸(分隔值(列表宽度高度));深度。(";255";));;图像内容(每个分隔值(向量列表像素))

如果你去掉我漂亮的格式,那只有12行代码,所有这些代码都是R5RS兼容的。我们也可以访问Netpbm套件,所以如果我们需要PNG,我们总是可以。/write-ppm|pnmtopng>;test.png。Netpbm是真正的隐藏宝石。嗯,至少对我来说是隐藏的。

如果你不熟悉Scheme,那么它在吸引学术类型方面有点自封的名声。它也是我所知道的最固执己见的语言之一;所有令人感兴趣的规范的开头都断言,编程语言不应该通过在功能之上堆积功能来设计,而应该通过消除使额外功能显得必要的弱点和限制来设计。该方案接受纯净和简单的方式清楚地表明,它是由数学书呆子设计的。(嘿,我也是个数学书呆子。(放松点。)。

正如我刚才提到的,有规格。当然,有几个。方案标准的发展是以线性方式开始的:RRS、→、RRRS、→、R3RS、→、R4RS、→、R5RS。我喜欢把这看作是经典的方案。但是到了修改R5RS的时候,随后的R6RS的批准引起了一些争议。它是臃肿的,还是什么的。差不多是这样。因此,当到了设计R7RS(小)的时候,计划语言指导委员会决定让语言分叉,从早期的R5RS开始作为一张白板[4]。这样一来,讨厌R6RS一切的书呆子可以为所欲为,喜欢R6RS的书呆子也可以为所欲为。计划虽有分歧,但却是和平的。哦,现在有一款正在进行中的R7RS-Large。ಠ_ಠ。

这些标准都非常短。R5RS长达50页。R7RS更大(n≈88)[11],R6RS更大(n≈163)[11],但它们仍然比我所知的任何其他语言规范打卡的页数都少。你不能把一大堆东西打包成50页,所以有一个事实上的标准库:Scheme Requests for Implementation,简称SRFI。

因为我对R6RS有点偏爱,所以我从R7RS开始这次旅行,认为回到R6RS的问题上会让我对它真正带来的东西有一个感觉。目前已经有一些R7RS的实现。我试过的是鸡肉,它不是正式的R7RS方案,但作为一个鸡蛋(库)支持R7RS标准。这花了一些力气,但我确实找到了在Emacs为鸡肉工作的公司式&Amp;朋友。将鸡蛋安装到非默认位置的文档已过期,但是如果您将系统库复制到您的CHICY_INSTALL_REPORATION,您将会失败。关于Geiser(或者更准确地说,方案模式)有一个小问题:它似乎不能正确地突出显示或缩进用户定义的宏。也许这是我有朝一日可以解决的问题。

选择业余爱好者计划实施的一个缺点是他们是久经沙场的。在写这篇文章时,我设法在最新版本的FICE中发现了一种回归,在该版本中,我的过程被以错误的顺序使用参数调用。因此,至少在本文中,我使用的是4.13.0。绅士们也还没有吃到鸡肉5号,但就稳定性而言,这也许是件好事。

参数化,这是我在Guix中处理的东西,我几乎忘记了。如果你不熟悉它,我能描述的最好的方式就是模拟动态作用域。

何时和除非,这些对于您自己实现来说都是微不足道的,但是不必编写它们总是很好的。

当然,R7RS还有更多内容,但这些都是我最感兴趣的。规范在第77页有一个标题为“语言更改”的部分,其中概述了与R5RS和R6RS的不兼容性,以及对R5RS的添加。

关于R7RS规范的话题,我认为这本书值得任何在一定程度上从事技术写作的人阅读,即使你不太关心Scheme-就像K&;R3即使你不关心C-它们都是简洁但不牺牲可理解性的很好的写作范例。设计的选择也是经过深思熟虑的,我认为这是值得欣赏的。例如,它们只支持通用可移植的文件系统操作[5]。这意味着不支持创建或操作目录。这样的限制听起来可能很原始,但提供可移植文件系统抽象的常见替代方案相当令人不快。如果您需要以这种方式操作目录,请寻找POSIX接口,而不是文件系统接口。

卫生宏观系统自R5RS以来一直在Scheme中,但这是我第一次实际使用它。我用def宏用Common Lisp和Emacs Lisp编写了很多宏,但这是一种新鲜空气。

(DEFINE-SYNTAX(SYNTAX-RULES())((ve3-bind((Namesvec)...))。正文)(let-Values((名称(Values(ve3-x vec)(ve3-y vec)(ve3-z vec)...)。正文)。

这在第一次尝试时就起作用了。一旦你阅读了关于它的教程,它就比手工构建一个AST更有意义。下面是一个稍微不那么琐碎的例子:

(定义-语法(语法-规则()((可能-绑定((名称选项)...))。身体)(如果(每个都是-一些?(列表选项...)。(let((name(展开选项))...)。正文)。

我知道,我知道。这不是处理选项类型的正确方式。我应该温习一下在Guix中的mlet*实现。但这足以满足我需要做的事情。

想想看,我选择为动态类型语言创建选项类型有点奇怪,不是吗?铁锈显然让我渴望能够映射出逻辑上等同于选项的东西,在我写这篇文章的时候,我还没有想到SRFI-2之类的东西。

总而言之?用普通的R7RS编写光线跟踪器相当容易。我最大的抱怨就是调试。鸡肉基本上没有堆积痕迹。它有一个调用历史,但这几乎没有给出从哪里调用某个东西的上下文。也没有行号。

R7RS实际上从R6RS中汲取了相当多的东西,而且两者在很大程度上都向后兼容R5RS。因此,我应该能够使用像Chez这样的R6RS实现来运行我的raytracer的R7RS版本,对吗?在很大程度上,是的。我只需要处理两件事:Error现在接受一个";who参数,而R7RS的定义记录类型几乎与R6RS中的等价物完全不同。

这在最近的承诺中没有体现出来,但也有一些我认为需要改变的鸡肉中不标准的东西。在Chez和其他模式中,嵌套定义绝对需要是表单中的第一件事。这与我在过程开始时放入的小文档字符串不兼容,在我正在使用的实现中,无论如何都不要做任何事情。我也在使用SRFI-1的EVER,但我将其替换为调用不同名称的标准R6RS过程。

关于R6RS记录的主题,它们最终没有R7RS那么冗长。以下是记录定义在R7RS中的显示方式:

您不会从上面的示例中猜到它,但是R6RS中的定义记录类型非常灵活。以上是以下的速记。

戈兰·温霍尔特(Göran Weinholt)写了一篇文章,比较了R7RS和R6RS。他在书中提到,R7RS定义记录型冗长的原因是宏系统无法创建新的标识符。在我的书中还有一个关于句法的问题,他的文章也不经意地提到R6RS记录系统已经被公开化了,但是我在正式的评论或其他地方找不到任何东西。我觉得这太棒了。

与R7RS非常相似,R6RS规范中有一节专门介绍语言更改。";这是附录E,给那些在家学习的人看,这一节与R7RS中的同等章节惊人地相似。他们的目标似乎是以略微不同的方式来解决同样的问题-允许用Scheme编写大型的、不是无关紧要的程序。

与R7RS不同,R6RS有一个标准的Reduce过程。嗯,用的是不同的名字。它有向左折叠和向右折叠,这是Reduce的更一般版本。就像什么时候和什么时候,除非,Reduce对于你自己来说是微不足道的,但是把它放在指尖是很好的。

(DEFINE(过程列表初始化))(DEFINE(列表结果)(IF(NULL?List)result(Reduce-ITER(Cdr List)(proc result(Car List)(Reduce-ITER list init)。

啊,是的。这是一些非常典型的Scheme代码。我还没有提到它,但是Scheme的实现必须是尾递归的[6]。上述过程应该在AMD64上编译成一个良好的ol&jnz循环。即Reduce-ITER实际上并不执行对其自身的函数调用。

(RNRS列表(6))包含我关心的大多数SRFI-1程序。这里有for-all而不是Each,我最初认为这对我的品味来说太接近for-each了,直到我意识到与eXist的对称性(这两个函数在命题逻辑中代表∀和∃)。R6RS拥有R7RS的所有很酷的东西,比如何时、除非、case-lambda和字符串端口。

据我所知,R7RS和R6RS库系统之间的差别很小。R6RS要求按该顺序在库的开头放置导出和导入表单,但导入和导出规范实质上是相同的(除了RENAME、…之外)。。

Jakob@epsilon~$time bash-c&c#39;./r6rs-raytracer>;test.ppm';real 0m13.665suser 0m11.645ssys 0m1.875sjakob@epsilon~$time bash-c&c#39;./r7rs-raytracer>;test2.ppm';real 1m12.259s用户1m11.515ssys。

其中';r6rs-raytracer';是由chez-exe在opt-level 3上制作的。CHICKEN的主要目的是:Chez可执行文件是大框架的。

Jakob@epsilon~$strip r6rs-raytracerjakob@epsilon~$du-sh r6rs-raytracer1.7M r6rs-raytracerjakob@epsilon~$strip r7rs-raytracerjakob@epsilon~$du-sh r7rs-raytracer236K r7rs-raytracer。

几乎所有的这些都来自于逐字逐句的请愿书。如果我仔细考虑,我可能会用我的代码和引导文件源代码编写一个工具来进行全程序死代码分析,但是1.7兆字节并不会让我呕吐。它放在软盘上是放不下的,但我见过大小约为千兆字节的戈比尼,所以情况可能会更糟。

鸡肉并不是最快的R7RS实现,而且我无论如何都在使用它的一个旧版本,所以对这个手摇的基准测试有所保留。如果您认为盗窃罪基准测试套件是一个公平的比较,那么本页将建议R7RS的沙鼠实现通常比Chez实现R6RS更快。该页面给我的主要启示是,这两个标准都有快速实现。

不幸的是,Chez上的堆栈跟踪情况甚至比鸡肉更糟糕。Guile更好,但上次我在Guix上使用它时,变量的优化让我非常头疼。我渴望有一个简单易懂的解释器版本。到一个月前,安迪·温戈已经想出了一些足够接近的东西,但我还没有机会试一试。

方案使用起来很有趣。R7RS和R6RS都是非常基础的,所以我觉得我需要花时间来熟悉已发表的SRFI;的子集,或者其他实用程序库,比如Gule的ICE-9,才能提高工作效率。在R7RS和R6RS中,R7RS和R6RS都是非常基础的,所以我觉得我需要花时间熟悉已发表的SRFI;的子集,或者其他实用程序库,比如Gule的ICE-9。从程序员的角度来看,R6RS似乎是两个中更好的一个,但它们足够相似,我可以合理地认为自己在这两个方面都很开心。如果我要使用一个方案,真正的问题将是我将使用哪种实现?这将反过来回答我的代码将符合哪种标准的问题。

据我所知,对于普通的李斯菲克人并没有像对阴谋家那样的过于简单化的刻板印象。但我想大多数人都会同意,Common Lisp是一种支持实用主义而不是纯粹性的Lisp方法-这并不意味着实用的软件不能用Schem.Like Scheme,

.