什么是一个好的REPL?(2017)

2020-09-26 03:09:19

亲爱的读者:虽然这篇文章提到了Clojure作为一个例子,但它并不是专门关于Clojure的;请不要让它成为语言战争的一部分。如果您知道可以提供高效REPL体验的其他配置,请在评论中描述它们!

我看到的Clojure与其他编程语言的大多数比较都是根据它的编程语言语义进行的:不变性、同质、面向数据、动态类型、一流函数、多态……。所有这些都是有趣而有价值的特性,但是真正让我选择Clojure for Projects的是它的交互式开发故事,它由REPL(Read-Eval-Print Loop)实现,它允许您在交互式shell中计算Clojure表达式(包括允许您修改正在运行的程序的状态或行为的表达式)。

如果您不熟悉Clojure,我将REPL描述为Clojure最与众不同的特性,您可能会感到惊讶:毕竟,现在大多数工业编程语言都附带了REPLS或shell(包括Python、Ruby、Javascript、PHP、Scala、Haskell等)。然而,我从未设法用这些语言重现我在Clojure中的REPL工作流程;事实是,并不是所有的REPL都是平等的。

在这篇文章中,我将试着描述REPL给你带来的好处,然后列出一些使一些REPL符合优点的技术特征。最后,我将试着思考哪些编程语言特性给REPLS带来了最大的影响力。

简短的答案是:通过提供紧密的反馈循环,并使您的程序看得见摸得着,REPL帮助您以显著更高的生产力和质量交付程序。如果你想知道为什么严密的反馈循环对编程等创造性活动很重要,我建议你看看布雷特·维克多(Bret Victor)的这篇演讲。

如果您不知道基于REPL的开发是什么样子,我建议您看几分钟以下视频:

我们编写的绝大多数程序基本上都是自动完成人类可以自己完成的任务。理想情况下,要实现复杂任务的自动化,我们应该能够将其拆分成较小的子任务,然后逐步将每个子任务自动化,直到达到完全自动化的解决方案。如果您要从头开始构建一台像计算机这样的复杂机器,那么在将各个组件组装在一起之前,您会希望确保了解它们是如何工作的,对吗?不幸的是,这不是我们在典型的写/(编译)/运行/监视-stdout工作流中得到的结果,在这种工作流中,我们基本上是盲目地将所有的片段放在一起,并祈祷它在我们第一次点击Run时就能正常工作。使用REPL的情况则不同:在运行整个程序之前,您将单独处理每段代码,这使您非常确信每个子任务都得到了很好的实现。

在另一个方向上也是如此:当一个完全自动化的程序中断时,为了调试它,您会想要手动重放子任务中的一些。

最后,并不是所有的程序都需要完全自动化-有时手动和自动化之间的中间地带正是您想要的。例如,REPL是一个很好的环境,可以运行对数据库的即席查询,或执行即席数据分析,同时利用您已经为项目编写的所有自动化代码-比使用数据库客户端要好得多,特别是当您需要查询多个数据存储或重新生成高级业务逻辑来访问数据时。

没有REPL的生活是怎么过的?以下是我们在没有REPL时为解决这些问题所做的一系列事情:

尝试使用cURL或数据库客户端等交互式工具,然后重现我们在代码中所做的事情。问题:您不能以任何方式将这些与您现有的代码库联系起来。这些工具擅长手动实验,但是您必须一直编写代码,以便在使用这些工具和在您的项目中使用它们之间架起一座桥梁。

运行调用我们代码库的脚本以打印到标准输出我们的文件。问题:在编写脚本之前,您需要确切地知道要输出什么;您不能停留在程序状态并从那里即兴发挥,正如我们将在下一节中讨论的那样。

使用单元测试(可能带有自动重新加载),它在这方面有很多限制,我们将在本文后面看到。

软件编程主要是探索性的活动。如果我们在编写程序之前对程序应该如何工作有一个精确的想法,我们就会使用代码,而不是编写代码。

因此,我们应该能够递增地编写我们的程序,一次一个表达式,计算出每一步下一步要做什么,引导机器通过我们当前的思维。这根本不是编译/运行全部内容/查看日志工作流为您提供的功能。

特别是,这种能力至关重要的一种情况是在紧急情况下修复错误。当您必须重现问题、找出原因、模拟修复并最终应用它时,REPL通常是分钟和小时之间的差异。

有趣的事实:也许这种情况最壮观的事件是在1999年修复了深空1号探测器的一个错误,幸运的是,当它在距离地球几光分钟的地方偏离航线时,恰好运行的是Common Lisp REPL。

自动化测试对于表达您的代码应该做什么非常有用,并使您确信代码能够正常工作并保持正常工作。

然而,当我看到一些TDD代码库时,在我看来,许多单元测试主要是为了在开发时使代码更加有形,这与使用REPL是相同的价值主张。然而,将单元测试用于此目的会带来很多问题:

单元测试太多会使代码库更难发展。理想情况下,您希望尽可能少的测试捕获尽可能多的域属性。

考试只能回答封闭式问题:这个能用吗?但不能用?它是怎么工作的?这是什么样子的?等等。

测试通常不会在真实条件下运行:它们将使用简单的人工数据和模拟服务,如数据库或API客户端。因此,它们通常不会帮助您理解仅发生在真实数据上的问题,也不会让您相信它们模拟的服务的真实实现确实有效。

因此,在我看来,许多单元测试的编写是因为缺乏更好的交互性解决方案,即使它们并没有真正作为单元测试发挥其应有的作用。当您拥有REPL时,您可以选择只编写重要的测试。

更重要的是,REPL帮助您编写这些测试。一旦您研究了REPL,您只需复制并粘贴一些REPL历史,即可获得示例数据和预期输出。您甚至可以使用REPL通过编程方式生成装备数据来帮助您为测试编写装备数据(每个手工编写全面装备数据集的人都知道这会有多乏味)。最后,当编写测试需要实现一些非常重要的逻辑时(就像进行基于属性的测试时一样),编写代码的REPL的生产力优势也适用于编写测试。

同样,不要据此认为REPL是测试的替代品。请务必编写测试,让REPL帮助您有效地编写正确的测试。

基于REPL的工作流鼓励您编写程序来操作易于编造的值。如果您需要在进行单个方法调用之前设置一个复杂的对象图,那么您不会很倾向于使用REPL。

因此,您将倾向于编写可访问的代码-几乎没有依赖项、很少有环境耦合、高度模块化以及有形的输入和输出。这可能会使您的代码更清晰、更易于测试和更容易调试。

需要明确的是,这是对您的代码的一个额外的约束(它需要一些前期思维来使您的代码对REPL友好,就像它需要一些前期思维来使您的代码易于测试一样)-但我相信它是一个非常有益的约束。当我的汽车引擎坏了的时候,我很高兴我可以掀开引擎盖,拿到所有的部件--这当然给汽车设计师带来了更多的工作。

REPL使代码更容易访问的另一种方式是,它为初学者提供了丰富的实验场地,从而使学习变得更容易。这既适用于学习语言,也适用于加入现有的项目。

就像我上面说的,并不是所有的REPL都给你同样的力量。在尝试了各种语言和工具配置中的REPL之后,我认为REPL应该使您能够做的主要事情如下所示,从而最大限度地发挥您的影响力:

定义新行为/修改现有行为。例如,在过程性语言中,这意味着定义新函数,并修改现有函数的实现。

将状态保存到内存中。如果你不能保留你操纵的数据,你将会浪费大量的精力来重新获取它--这就像没有办公桌做文书工作一样。

输出值,这些值可以很容易地转换为代码。这意味着REPL输出的文本表示适合嵌入到代码中。

使您可以访问整个项目代码。您应该能够调用项目中编写的任何代码段的依赖项。作为一个执行平台,REPL应该尽可能地再现在生产中运行代码的条件。

把你放在你的代码的位置上。给定项目文件中的任何一段代码,REPL应该允许您将自己置于与该代码段相同的上下文中-例如,编写一些新代码,就好像它位于同一源文件的同一行中,具有相同的词法范围、运行时环境等(在Clojure中,这是由名称空间中的(in-ns...)-&39;-function提供的)。

与正在运行的程序交互。例如,如果您正在开发Web服务器,您希望能够同时从REPL运行Web服务器并与其交互,例如,更改路由的实现并在Web浏览器中查看更改,或者从Web浏览器发送请求并在REPL中拦截该请求。这意味着某种形式的并发支持,因为程序状态需要由至少2个独立的逻辑进程(机器事件和REPL交互)访问。

正在将REPL状态与源代码文件同步。例如,这意味着在REPL中加载源代码文件,然后查看它定义的在REPL中生效的所有行为和状态。

对编辑友好。也就是说,公开可由编辑器以编程方式利用的通信接口所需的功能包括语法突出显示、漂亮打印、代码完成、将代码从编辑器缓冲区发送到REPL、将编辑器输出粘贴到编辑器缓冲区以及提供数据可视化工具。(公平地说,这至少取决于REPL周围的工具,而不是REPL本身)。

我早些时候说过,Clojure的语义对我来说没有它的REPL那么有价值;然而,这两个问题并不是完全分开的。一些语言,因为它们的语义,或多或少与基于REPL的开发兼容。下面是我尝试列出使熟练的REPL工作流成为可能的主要编程语言特性:

数据文字。也就是说,程序中操作的值具有文本表示,该文本表示既可供人类阅读,又可作为代码执行。最著名的数据文字形式是JavaScript Object Notation(JSON)。理想情况下,编程语言应该习惯于编写大多数值都可以用数据文字表示的程序。

一成不变。在REPL中编程时,您既要保留计算结果,又要以序列化形式(输出中的文本)查看它们;此外,因为您正在做的大多数工作都是实验性的,所以您希望能够限制评估代码的效果(大多数情况下,除了显示结果并将其保存在内存之外,没有其他效果)。这意味着你在编程时会倾向于取值,而不是副作用。因此,使使用不可变数据结构进行编程变得实用的编程语言对REPL更友好。

顶级定义。在REPL工作包括(重新)定义全局数据和行为。一些语言对此提供的支持有限(特别是一些基于类的语言);有时,它们附带的REPL补丁为该语言提供了一些专门用于此目的的附加功能,但在实践中,这会导致REPL和现有代码库之间的阻抗不匹配-您真的应该能够将代码从一个无缝传输到另一个。更广泛地说,语言应该具有在程序运行时重新定义代码的语义-交互性不应该是语言设计中的事后考虑!

表现力。你可能会认为提到这件事有点傻,但这不是必然的。对于我们所追求的复杂程度,我们需要我们的语言具有清晰而简洁的语法,这些语法可以表达我们知道如何高效运行的强大抽象,而没有能够弥补这些需求的交互性级别。这就是为什么我们不把大部分程序写成Bash脚本的原因。

如果你曾经在舞台上演奏过现场音乐,却听不到自己的乐器,那么你很清楚我在没有REPL的情况下编程时的感受--无能为力和缺乏自信。

我们喜欢从编程语言和库提供的抽象的角度来讨论它们的优点-但是我们必须承认工具扮演着同样重要的角色。我们中的大多数人都在高级编辑器、调试器和版本控制等方面经历过,但很少有人有机会在功能齐全的REPLS中体验过。希望这篇博客文章将有助于纠正这个错误:)。