LISP和Haskell(2015)

2020-10-08 15:20:24

Haskell、Lisp和Haskell可以说是一些比较独特的语言。比较语言总是很有趣的,所以让我用一个故事来娱乐你们,我是如何最终决定哪种语言更好的。

当我第一次发现Common Lisp时,它让我大吃一惊。说真的,Lisp具有一致的语法、良好的设计和独特的元编程功能。在Common Lisp之后,我学习了其他几种语言,有些是出于需要,有些是出于好奇心:Python、JavaScript、Prolog、Clojure和Haskell。我以前也做过C和C++,但现在不碰了。直到最近,我还认为Common Lisp是我所知道的最好的语言,可能也是现存的最强大的语言。

事实是,我知道Common Lisp是什么以及它能做什么,但是我真正(或多或少)经常攻击Lisp的日子已经一去不复返了,这些天我主要在做Haskell。

今天,我实际上有机会将我的工作效率与CommonLisp和Haskell进行了比较。我决定在我的开源项目上花几个小时,首先,我重构了兆秒,这很容易,但是我没有注意到这一点,因为我已经习惯了Haskell给我的效率水平。

接下来,我的一个Common Lisp库的一个用户打开了一个问题,要求稍微改进一件事。我在15分钟的时间内估计了所需的工作,并在几个月内第一次开始了Common Lisp编程。

大约花了1个小时写了大约20行琐碎的代码。当然,有人可能会说,我只是忘了细节。然而,在我看来,真正的原因是:

Common Lisp是动态类型的,当您编写代码时,编译器无法帮助您。(嗯,它可以为您提供一些帮助,确保您的代码在语法上是正确的,并且您声明的所有变量都用于某些用途。)。

Common Lisp将功能代码与有副作用的代码混合在一起。在编写惯用的Common Lisp时,通常必须混合使用功能代码和非功能方法。下面看看这是如何工作的。

按照现代标准,Common Lisp的标准库(作为ANSI Common Lisp标准的一部分提供给您的函数)相当差,缺少许多有用的函数。有图书馆,不过我会去的。

对于我正在开发库来说,具有最小的依赖性是很重要的,所以我用Bare Common Lisp编写了这个函数来添加填充到除第一行之外的每一行文本:

(deFun add-text-pending(字符串&;键填充换行符)";将填充添加到文本STR。除第一行外,每一行都将以填充空格作为前缀。如果newline为非空,则换行符将优先于文本,使其从下一行开始,并对每一行应用填充。";(let((str(if newline(CONCATENATEATE';string(string#\N ewline)str)(with-output-to-string(map';String(lambda(X)(Princ X S)(When(char=x#\N ewline)(dotime(i填充)(princ#\S空格)。

如果您不会说Common Lisp,让我重点介绍代码的一些部分:

Conatenate需要知道其输出的类型,所以我们向它传递一个符号,指定所需结果的类型作为第一个参数。

(STRING#\NEWLINE)构造包含单个换行符的行。Common Lisp中没有语法来编写类似";\n";的代码。另一种方法是(格式为nil";~%";)。如果要将所有其他特殊字符放入字符串中,则没有适用于所有其他特殊字符的语法。公平地说,您有多行字符串文字,而没有有趣的转义,这对于文档字符串等非常重要。

(MAP';String…)。用于循环访问字符串中的字符。请注意,这里我们使用map函数作为一个相当必要的过程的助手-使用临时创建的流s将结果打印到新字符串(在with-output-to-string的帮助下)。但这在Common Lisp中是自然的。

;SLIME 2015-10-18 CL-USER>;(asdf:LOAD-SYSTEM:UNIX-OPTS)T CL-USER>;(包内:unix-opts)#<;package";UNIX-opts";>;opts>;(defvar*foo*(格式为nil";第一行~%第二行~%第三行";))*foo*opts>;*foo*";第一行第二行第三行";;编译(DEFUN添加-文本-填充...)。OPTS>;(Add-Text-PADDING*foo*:Padding10);评估在#<;type-error-type:character datum:nil>;中止。

这是很难争辩的,零绝对不是一个角色。但是我到底为什么会得到这个呢?你看得出来吗?请尽你所能努力!(答案在博客文章的末尾。)。

我决定不再黑进Common Lisp了。这是一种很棒而且很有表现力的语言,但是我想用我高效的语言来写。

我几乎所有与文本相关的东西都使用Emacs。我特别喜欢的一个套餐是Flycheck。当我编辑Haskell源代码时,Flycheck在后台运行带有-Wall标志和HLint的GHC,并交互地在我的源代码下划线显示警告和错误。这对于任何语言来说都是一个方便的特性,但是只有Haskell及其类型系统将这种工具发挥到了极限。

事实上,这种不间断的与编译器的交互对话是我使用过的最高效的编程工作流。再加上这样一个事实,即如果您的代码编译成功,它可能会正常工作,Haskell肯定是效率最高(就人力资源而言)最不存在的编程语言,因为静态类型系统对程序员来说是一种功能强大的编程语言。当然,错误也可能存在于Haskell代码中,但我并不是说我们应该放弃编写测试。

说到测试,最近我发现Zach Beane AKA Xach,一个超级别的Common Lisp黑客,通常不编写测试。仅供参考,他是Quicklisp的作者,有点像(但不完全是)Cabal或Stack。Quicklisp实际上是Common Lisp世界中唯一被广泛使用的库管理器,所以它是用Common Lisp编写的,没有任何测试。它是怎么工作的,这对我来说是个奇迹。通常,当一个项目足够大的时候,我开始怀疑它的所有部分在经过一些更改后是否仍然有效,所以我无法想象你可以不经过测试就能做像Quicklisp这样的事情,并且对结果充满信心。

但是你知道吗,Lisp及其最高级的方言(IMO)Common Lisp真的很酷。如果你不相信我,你可以随时读保罗·格雷厄姆的书。作者可以在许多页面上告诉您Common Lisp是一门多么伟大的语言。我不记得我是在哪里读到这篇文章的,但他说过这样的话:“没有库的问题是存在的,但在一个足够大的项目中,语言本身的好处超过了库的缺乏。”

那么,就拿Python这样的高级语言来说吧,它拥有所有好的库,对于任何规模的项目来说,它都比Common Lisp要好。虽然缺少宏,但毕竟没有宏也可以。

普通的Lisp没有足够的高质量、积极维护的库,事实上,有一些珍珠,比如穴居人或树桩,但是大多数库看起来都不够好。有时您会开始想,如果您想要完成一个伟大的项目,您需要编写自己的库(您可能会这样做,就像您之前的许多人一样,但这并不是说它改善了情况)。

另一个问题是,一些广泛使用的Common Lisp库根本没有文档。如果您想了解如何使用它们,请阅读源代码。我可以说出其中几个的名字,但我不想这么做,因为我认为这是不礼貌的。我在Oneequite Popular库的GitHub上打开了一个问题,要求维护人员编写文档。六个月过去了,它仍然没有写出来(很奇怪,对吧?)。在我看来,这不是维护代码的严肃方法。

当我对Common Lisp感兴趣时,我有了一个特别喜欢的项目,可以帮助我记住各种法语单词和动词。当然,我想把整件事做得得体,即使它是控制台应用程序,它也应该有像样的界面,总体上工作起来也要流畅。我成功了,但是如果我用Python写的话,我需要做的事情要多得多,Python说,这就是为什么(回想起来我明白)功能较弱的Python会更适合这个(或几乎任何)项目。

有一篇名为“动态语言都是静态语言”的博客文章。简而言之,作者认为动态语言是静态语言,但有一个巨大的类型,包括所有可能的值。以下是我认为重要的一段话:

这正是动态类型语言的错误所在:它们没有提供忽略类型的自由,而是强加了将注意力限制在单一类型的束缚!每个单值都必须是该类型的值,您别无选择!即使在特殊情况下,我们绝对确定某个特定值是一个整数,我们也别无选择,只能将其视为被分类为整数而不是类型化的“一元类型”的值。从概念上讲,这只是垃圾,但它有严重的、切实的惩罚。首先,你剥夺了自己声明和实施不变的能力,即特定程序点的值必须是整数。另一方面,为了表示类本身(某种类型的标记),并在每次使用时检查、删除和应用值上的类标记,您会强加一些严重的运行时开销。

缺乏在类型级别表达程序含义的能力是Lisp的另一个缺点。(您也可以在Common Lisp中添加类型,但这仅用于优化。顺便说一句,普通的Lisp几乎可以和C一样快。)。不幸的是,大多数静态类型语言没有足够强大的类型系统来作为您的盟友工作,而不是仅仅为了告诉您代码无法编译而存在的敌人。一旦你有了强大而灵活的排版系统,它就会让人上瘾。

当然,编程(在很大程度上)不是数学。有些东西对某些人来说“在概念上只是垃圾”,但是从实际的角度来看,它们突然给了您一些类似于Lisp宏的东西。我可以肯定的是,我再也不能确定使用动态语言编写代码了。

现在,人们很容易认为Lisp可以通过强静态类型以某种方式进行增强。球拍有静态打字的方言。问题是它的类型体系是否足够先进?我的意思是,它允许表达非平凡的不变量吗?它如何处理宏,等等?我不知道,如果你同时用了瑞克特和哈斯克尔,请联系我,你可以做个比较。

Lisp的销售特性(如动态重新编程和Lisp宏)与该语言的动态性紧密相关。LISP就像一个活的有机体,它充满了反思,编译时和运行时没有区别。这就是为什么可以用Lisp动态处理Lisp代码的原因。例如,有一个模板Haskell,它允许您进行元编程,并且它是类型安全的,但这是静态元编程,这并不完全相同。

我认为让整个Lisp静态键入是很困难的。从非常基本的东西,如囚犯细胞,这一切都是动态的。有一点是明确的,具有强大而强大的静态类型系统的Lisp可能与我们熟悉的Lisp非常不同。

我的想法是编程语言有两个阵营:

破解语言,一切皆有可能,你可以做任何你想做的事(潜在的坏事)。

当我还小的时候,我喜欢黑客语言。C绝对是一种黑客语言。如果你想要一个保护大致相同水平的语言的例子,那就是帕斯卡。

在俄罗斯,帕斯卡语作为你在学校、学院或大学学习的第一语言特别受欢迎。我讨厌帕斯卡。我自学了C语言,因为它看起来更硬更真实。然后我学会了用C++做作业(通常我们被允许写任何东西,当我开始用考官根本看不懂的语言Common Lisp做作业时,我就滥用了这一点)。

现在我感谢那些保护我的语言,也许是因为我知道我肯定会犯错误,我想要一些东西来捕捉它们。哈斯克尔改变了我的程序员身份。现在我认为Pascal有一些好东西(除了知道它们长度的字符串),至少我不再讨厌它了。

如果您还很好奇,当使用作为第三个参数提供的Sequence中的元素进行调用时,map尝试使用其第二个参数返回的值组装指定的(作为第一个参数)数据类型(在我的例子中为String)。由于lambda在其主体中具有dotime作为其主体中的最后形式,因此整个函数总是返回nil。NIL不是字符,我们不能从NIL构建字符串。

因此,修复方法是将nil作为map的第一个参数传递,表明我们只想返回nil,而不构建任何字符串。

我把绳子放在那里,想着我要穿越的绳子。大错特错?也许是吧,我只是个普通人。对我来说,问题的原因并不明显,直到我发现了发生了什么,这花了一段时间。想想哈斯克尔,这个问题将无处藏身。