为什么我还是Lisp

2021-01-31 21:20:56

作为Scheme / Common Lisp / Racket的长期用户(并且是积极的支持者),有时会被问到为什么我坚持使用它们。幸运的是,我一直领导着自己的工程组织,因此我从来不必将其证明为“管理”,但是我自己的工程学同事中还有更重要的选区,他们从未有过使用这些语言的乐趣。尽管他们从不要求理由,但他们的确出于好奇心而问,有时也想知道为什么我不打算放弃即将加入Python或Scala的下一个很酷的功能,或者他们本月的口味如何。

尽管使用的Lisp的实际味道对我来说有所不同(方案,Common Lisp,Racket,Esplang的Ersp),但核心始终保持不变:基于s表达式,动态类型化,主要是功能化的,按调用基于λ微积分的语言。

我从十几岁开始就在ZX Spectrum +上使用BASIC进行认真的编程,尽管我以前曾涉足(手工)编写Fortran程序。这对我来说是决定性的时期,因为它真正地定义了我的职业道路。我很快就将语言推到了极限,并试图编写超出该语言及其实现的有限能力的程序。我移到Pascal片刻(在DOS盒子上用Turbo Pascal),这很有趣,直到我在Unix上发现C为止(Santa Cruz Operation Xenix!)。那使我获得了计算机科学学士学位,但这总是让我渴望在程序中表现出更多表现力。

那是我在Miranda(丑陋的Haskell的非常漂亮的妈妈)中发现函数式编程(谢谢IISc!)的时候,它让我大开眼界,想在程序中获得美感。我在编程语言中表现力的概念开始有了长足的进步。我对程序外观的概念现在开始包含简洁,优雅和可读性。

Miranda并不是一种特别快的语言,因此执行速度是一个问题。 Miranda还是具有Standard-ML样式类型推断功能的静态类型语言。一开始,我迷上了类型系统。随着时间的流逝,人们开始鄙视它。虽然它帮助我在编译时捕获了一些东西,但大部分都被阻碍了(稍后再介绍)。

大约一年后,我最终与Dan Friedman(《 Little Lisper》 /《 The Little Schemer》一书的作者)一起在印第安纳大学学习编程语言。这是我对Scheme(以及Lisp的世界)的介绍。我终于知道,我已经找到了表达自己的程序的理想媒介。在过去的25年中,它没有改变。

在本文中,我试图解释甚至探讨为什么会这样。我只是一个不会改变自己方式的古老恐龙吗?还是我太傲慢而鄙视新想法?还是我感到疲惫?我认为答案并非以上所有。我发现了完美,还没有什么可以解决的。

让我们分解一下。我说了几段:

所有程序中的基本实体都是一个功能。功能具有它们的意向性,它们构成了软件设计过程的基础。您一直在思考如何对信息进行操作,如何对其进行转换以及如何对其进行生产。我还没有找到一个基本框架,该框架可以捕获比λ微积分更好的内在意图(“方式”)。

意图一词可能使你们中的一些人失望了。数学有两种方法来考虑功能。首先,作为一组有序对:(输入,输出)。尽管这种表示形式是证明有关函数定理的好方法,但在编码时完全没有用。这也称为功能的扩展视图。

考虑功能的第二种方法是作为转换规则。例如,将输入本身乘以得到输出(这为我们提供了平方函数,每种编程语言都将其缩写为sqr)。这是函数的有意视图,λ演算很好地捕捉了该函数,并提供了非常简单的规则来帮助我们证明关于函数的定理,而无需借助可扩展性。

现在,请稍等,我确定您正在考虑。我从来没有证明我的职能。我敢打赌你有。而且您一直都在做。您总是会说服自己,您的职能正在做对的事情。您的代码可能不是正式的证明(可能会导致一些错误),但是对代码进行推理是软件开发人员一直在做的事情。他们正在脑海中回放代码,以查看其行为。

基于λ微积分的语言非常容易在您的脑海中“回放代码”。 λ演算的简单规则意味着您需要携带的东西更少,并且代码易于阅读和理解。

编程语言当然是实用的工具,因此必须增强其核心简单性以适应更广泛的目的。这就是为什么我喜欢Scheme(以及我目前最喜欢的Scheme,Racket [CS,对于那些关心此类事情的人])。它增加了核心λ微积分,是使其可用的最低限度。即使是加法运算,也遵循λ微积分所支持的基本原理,因此几乎没有惊喜。

当然,这确实意味着递归是一种生活方式。如果您是那些对递归从来没有道理的人之一,或者您仍然认为“递归效率低下”,那么现在是时候重新讨论它了。 Scheme(和Racket)尽可能有效地将递归实现为循环。不仅如此,Scheme标准还要求它。

此功能被称为尾部调用优化(TCO),已经存在了几十年,这是对我们编程语言状态的可悲评论,没有现代语言支持它。 JVM尤其存在问题,因为出现了尝试将JVM定位为运行时体系结构的更新语言。 JVM不支持它,因此,基于JVM构建的语言必须跳过障碍,以提供有时适用的TCO的外观。因此,我总是非常怀疑地使用任何针对JVM的功能语言。这也是我没有迷上Clojure的原因。

这就是原因之一。 Scheme / Racket是基于λ微积分的编程语言的非常明智的实现。您可能已经注意到,我使用的不是功能语言一词来描述Scheme。那是因为虽然它主要是功能性的,但并不会一直偏向不可变异性。尽管不鼓励使用,Scheme意识到在某些真正的情况下可能会使用突变,并且它允许在不使用辅助设备的情况下进行突变。在这里,我不会与纯粹主义者争论为什么或为什么不是一个好主意,但这与我稍后将在本文中讨论的内容有关。

那些知道λ微积分细节的人可能已经认识到我为什么选择进行这种区分。记住我的历史:我在Miranda上是个懒惰的功能语言(就像Haskell一样)。这意味着仅在需要表达式的值时才对其求值。这也是定义原始λ演算的方式。这意味着,函数的参数在使用时(而不是在调用函数时)进行求值。

这种区别是微妙的,并且确实具有一些很好的数学特性,但是对您脑海中的“回放代码”具有深远的影响。在很多情况下,这种情况会让您感到惊讶(即使是经验丰富的程序员),但在某些情况下,您可能会比其他人更感兴趣。

作为程序员,在您的职业生涯中最难处理的错误之一就是那些在屏幕上打印某些东西会使该错误消失的错误。在惰性函数式语言中,打印某些内容会强制对表达式进行求值,就像在越野车中可能没有求值那样。因此,将值打印为调试工具变得令人怀疑,因为它会严重改变程序的行为方式。我不了解您,但对我而言,打印是一种工具,有人必须用冷的,不牢固的手指撬开它。

在语言中随处使用惰性评估还有其他一些细微之处,这使得它对我而言吸引力较小。我永远都不想猜测何时要评估某个表达式。要么评估,要么不评估。不要让我猜测什么时候,特别是如果它会在某个库的深处发生(或不会发生)。

按值调用对如何证明有关程序的形式定理有一些影响,但值得庆幸的是,存在一种称为按值调用λ微积分的野兽,我们可以在必要时依赖它。

通过使用thunk和mutation,Scheme允许您进行显式的懒惰评估,可以方便地将其抽象化,以便您在需要时进行按需调用。这使我们进入了下一个阶段。

函数式编程很棒。在您的脑海中回放功能代码很简单;该代码易于阅读,并且无需担心突变。除非还不够。

我不赞成随意突变,但我赞成明智地使用突变。像上面的惰性评估示例一样,我可以完全支持使用突变来实现功能。突变存在于所有软件的外围。对于某些抽象,最富有表现力的事情可能是将变异引入一个不错的小抽象中。例如,消息传递总线是一种充满突变的抽象,但是它可以具有非常优雅的纯功能代码段,而不必携带虚假的状态变量或诸如monad之类的辅助设备。

像其他任何工具一样,将极端的代码变成极端有害。一种允许我明智地使用突变来以更优雅的方式实现大量代码的语言,总是会比在任何情况下都强制(大多数情况下)构造的语言赢得更高的评价。

因此,Scheme固有的偏向于不变异,但是它对变异(或称其为副作用)的“如果需要的话就使用”的态度使它对我来说是更有效的工具。

我在上面提到了monad,因此最好先讨论一下monad,因为它们是获得效果的纯功能方式。在写完有关它们的博士学位论文后,我想我对它们有所了解。我喜欢Eugenio Moggi最初创作的Monads的优雅和纯粹的美。将计算与该计算所产生的值分离,然后将该计算归类为一个类型的想法在每个词的意义上都是极好的。这是数学上理解编程语言语义的好方法。

作为编程工具,我对此充满了感慨。当您可以轻松创建简单的抽象来简化程序的其余部分时,这是一种隔离效果,然后将其贯穿整个程序的复杂方法。作为一位杰出的类型理论家(他将保持无名状态)曾经说过:“单子仅在每个其他星期二有用”。

Monad是一种辅助设备,必须使用功能语言来提供副作用的功能围栏。问题在于,围栏是“传染性的”,接触围栏的所有物品现在也必须被围起来,以此类推,直到到达操场的尽头。因此,您现在不必面对副作用并优雅地进行抽象处理,而是获得了一个复杂的抽象,您被迫随身携带到任何地方。最重要的是,它们的组合也不太好。

我并不是说Monad完全没有用。它们在某些情况下(“每隔一个星期二”)运行良好,我在工作时会使用它们。但是,当它们是进行计算的唯一机制时,它们会严重削弱编程语言的表达能力。

当今世界正围绕着类型化语言不断发展。 TypeScript被认为是JavaScript顽强的世界中的救星。 Python和JavaScript因缺乏静态类型而受到谴责。在大型编程项目中,类型被认为对于文档和通讯至关重要。工程经理将自己推到类型推断的脚上,以保护他们免受产生劣质代码的普通软件工程师的侵害。

静态类型有两种。在C,C ++,Java,Fortran中使用“旧式”静态类型,其中编译器使用这些类型来产生更有效的代码。这里的类型检查器有严格的限制,但是除了基本的类型检查之外,不要假装提供任何保证。它们至少是可以理解的。

然后是一种新的静态类型,它起源于Hindley-Milner类型系统,它带来了新的野兽:类型推断。这给您一种幻觉,即并非所有类型都需要声明,并且如果您按照规则行事,则可以享受老式静态类型的好处,还可以享受到诸如多态之类的一些很棒的新功能。这种观点也是可以理解的。

但是在最近的几十年中,它已经有了新的解释:静态类型是编译时错误检查的一种形式,因此它将帮助您生成质量更高的代码。就像静态类型是神奇的定理证明者一样,它将验证程序的某些深层属性。这就是我所说的bulsh * t。我从来没有使用过静态类型检查器(不管它有多复杂),除了明显的错误(无论如何都应该在测试中捕获)之外,它可以帮助我防止其他任何事情。

但是,静态类型检查器的作用是妨碍我的。总是。没有失败。作为程序员,我一直在脑海里随身携带不变式(这是我程序中有关事物的属性的奇特名称)。这些不变式中只有一种是事物的类型。当您初次遇到不变性时,拥有一个可以验证不变性的工具(就像我对Miranda所做的那样)。

但这是一个愚蠢的工具。它只能做很多事情。因此,您现在最终获得了有关如何满足此工具的人为规则。我知道的事情(可以证明我的用例是合理的,甚至可以正式证明)完全不是一件好事。因此,现在我必须重新设计程序,以满足有限工具的需求。大多数人都对这种折衷感到完全满意,并且他们会慢慢改变他们对软件的看法,以适应其局限性。

在古老的印地语电影中,检查面板不允许在屏幕上显示接吻。因此,浪漫的场景总是会被切成花朵相撞,或者成对的鸟儿一起飞走,或者像这样的傻事。这就是静态类型检查器的感觉。我们被赋予一种美丽的语言来保证我们享有言论自由的权利,但是随后我们被审查委员会维持对言论的警惕,所以我不得不说我在隐喻和象征中的意思仅是边际利益。

一款出色的工具所能做的就是让我在编译时陈述和证明我的所有不变量。当然,这最终是无法解决的。因此,考虑到在糟糕的工具(静态类型检查器)和没有工具之间进行选择,我一直倾向于没有工具,因为我希望对程序没有任何人为的约束。因此是动态打字。

所有程序(静态类型的程序或其他类型的程序)都必须处理运行时异常。写得很好的程序会遇到更少的情况,写得不好的程序会遇到更多的问题。静态类型检查器会将一些人从书写不佳的阵营转移到书写良好的阵营。严格的测试可以提高(并保证)软件质量。要提供高质量的软件,没有其他解决方案。因此,无论您是否进行静态类型输入,都只会对软件质量产生微不足道的影响,而当您拥有由周到的程序员精心设计的程序时,这种影响就会消失。

换句话说,静态类型是没有意义的。它可能具有某些文档价值,但不能替代其他不变量的文档。例如,您的不变性可能类似于我期望的那样,一个单调递增的数字数组,其均值是某某某物,而标准偏差是某某某物。静态类型检查最好让您执行的是“ array [float]”。其余的不变式必须用单词表示,作为该函数的文档。那么,为什么要遭受“ array [float]”的痛苦呢?

动态类型使我能够表达自己想要在程序中表达的内容,而不会遇到麻烦。我可以根据程序的需要将我的不变量指定为显式检查或文档。

但是,像其他所有内容一样,有时您需要静态地了解类型。例如,我经常处理图像,这有助于知道它们是“ array [byte]”,并且我已经预先烘焙了可以神奇地快速处理它们的操作。 Scheme / Lisp / Racket都提供了在需要时可以执行此操作的方法。在Scheme中,它取决于实现,但是Racket带有“ Typed Racket”变体,可以与动态类型变体混合使用。 Common Lisp允许在特定的上下文中声明类型,主要是让编译器在可能的情况下实现优化。

因此,再次,Scheme / Lisp / Racket让我在需要时可以受益于类型,但不要在任何地方强加约束。两全其美。

最后,我们得出了为什么我使用Lisp的最重要原因之一。对于以前从未听说过s-expression一词的人来说,它代表Lisp及其子代中特殊的句法选择。所有句法形式都是原子或列表。原子是诸如名称(符号),数字,字符串和布尔值之类的东西。列表看起来像“(…)”,其中列表的内容也可以是列表或原子,因此最好有一个空列表“()”。而已。

没有中缀运算,没有运算符优先级,没有关联性,没有虚假的分隔符,没有悬挂的东西,什么也没有。所有函数应用程序都是前缀,因此您不用说“(a + b)”,而是说“(+ a b)”,这进一步使您可以灵活地说出“(+ a b c)”之类的东西。 “ +”只是您可以根据需要重新定义的函数的名称。

有一些“关键词”可以指导给定列表以某种方式进行评估,但是评估规则是分层的并且定义明确。换句话说,s表达式实际上是程序的基于树的表示形式。

这种语法的简单性经常使新手感到困惑,并且可能已经关闭了许多不幸的程序员,他们没有被这种编写程序的方式所吸引。

这种语法形式的最大优点是极简主义-您不需要虚假的语法结构即可传达概念。通过使用的函数名称或语法关键字完全传达概念。这产生了奇怪的紧凑代码。在字符数方面并不总是紧凑的,但是在阅读代码时需要记住的概念数量上却很紧凑。

而且还不到一半。 如果程序是树,则可以编写程序来操纵这些树。 Lispers(以及Schemers和Racketeers)称这些为宏或语法扩展。 换句话说,您可以扩展语言的语法以引入新的抽象。 几代Lispers编写了无数酷炫的语法扩展,包括对象系统,语言嵌入,专用语言和其他。 我用它来开发语法功能,使我可以使用Scheme来构建从传感器网络到数字信号处理再到电子商务定价策略的整个领域。 世界上没有其他语言能够接近这种语法扩展水平,这是我(和 ......