Why I Still Reach for Lisp (& Scheme) Instead of Haskell

Why I Still Reach for Lisp (& Scheme) Instead of Haskell

为什么我依然选择 Lisp(和 Scheme)而非 Haskell

There is a persistent tension in software engineering between the beautiful, mathematically pure ideal of a program, and the messy, pragmatic reality of just getting things done. Over my career, I’ve explored the depths of both extremes in an attempt to find my personal sweet spot for hacking. 在软件工程领域,程序那数学般纯粹的理想之美与“搞定事情”这种杂乱且务实的现实之间,始终存在着一种挥之不去的张力。在我的职业生涯中,我探索了这两个极端的深处,试图找到属于我个人的编程“甜蜜点”。

Before you sharpen your keyboards and start a flame war over the title, let me point out that I haven’t written this post to talk bad about Haskell, or any other tool for that matter. In fact, I love Haskell. I taught myself, banged my head against the wall over the course of three years, and built several real-world projects with it (some even became a bit lucrative). Between my time in the web development world, the Go world, the JVM world with Java, Scala and Kotlin, and my long history hacking in Lisp (Emacs, Common, Scheme), I have come to deeply appreciate functional programming. 在你们磨好键盘准备因标题引发一场口水战之前,请允许我说明:写这篇文章并非为了诋毁 Haskell 或任何其他工具。事实上,我热爱 Haskell。我曾自学它,在三年时间里撞得头破血流,并用它构建了几个实际项目(其中一些甚至还带来了一些收益)。在经历了 Web 开发、Go 语言、JVM 生态(Java、Scala 和 Kotlin)以及我长期使用 Lisp(Emacs、Common、Scheme)进行编程的历程后,我已深深领悟了函数式编程的精髓。

Enlightening as it can be

虽具启发性

Haskell has what likely is the most amazing, enlightening and complex type system to work with (as do more ML languages). It is also the undisputed king of introducing mathematical ideas and concepts to programming, and popularizing them. Haskell circles are frequented by PhDs, computer science researchers, category theorists and all kinds of smart people (don’t underestimate other communities, like Schemers though). Some of the amazing innovations of Haskell or that it has helped popularize, which blew my mind several times: algebraic data types, pattern matching, functors, monads, monoids, semigroups, effectful computation modelled as monads, purely functional domain-specific languages (DSLs), etc. All these kind of things often feel bolted-on or missing entirely in other languages! For all its brilliance, Haskell resists most of the attempts people make to just hack and write useful code quickly. Specially people new to functional programming (or god forbid new to monads and functors! A monad is just a monoid in the category of endofunctors, what’s the problem?) Haskell 拥有目前最令人惊叹、最具启发性且最复杂的类型系统(其他 ML 系语言也一样)。它也是将数学思想和概念引入编程并将其普及的无可争议的王者。Haskell 圈子里活跃着博士、计算机科学家、范畴论学者以及各种聪明人(不过也别小看其他社区,比如 Scheme 社区)。Haskell 的一些惊人创新,或者它帮助普及的概念,曾多次让我大开眼界:代数数据类型、模式匹配、函子(functors)、单子(monads)、幺半群(monoids)、半群(semigroups)、以单子建模的副作用计算、纯函数式领域特定语言(DSLs)等等。所有这些在其他语言中往往感觉像是硬塞进去的,甚至完全缺失!尽管 Haskell 才华横溢,但它却抵触人们想要快速“黑”出有用代码的尝试。尤其是对于函数式编程的新手来说(天哪,更别提那些刚接触单子和函子的人了!“单子不过是自函子范畴上的幺半群,这有什么难的?”)。

When pragmatism enables actual productivity

当务实带来真正的生产力

Scheme (and Lisp in general) might lack Haskell’s innovations and purity, favoring a minimalistic flexibility instead, but it mixes practicality with functional beauty in a way that makes it a functional language for human beings. Actually, in my opinion, Scheme (and Lisp) allows you to express complex systems and problem domains in more simple terms than any other language can. Take a recent adventure of mine, for example. I was spinning up a prototype for a bookmark management tool, just one of many projects I’ve come up with over the years. I started in Haskell as I thought the beauty of data modelling and pure side-effect-free reasoning would work well: it’s also fast, elegant, and once you’ve used modules like Parsec, Servant, and optparse-applicative, it’s tough to imagine writing certain things, like a parser, without it. Scheme(以及广义上的 Lisp)可能缺乏 Haskell 的创新和纯粹,它更倾向于极简的灵活性,但它将实用性与函数式之美融合在一起,使其成为一种“适合人类”的函数式语言。事实上,在我看来,Scheme(和 Lisp)能让你用比任何其他语言都更简单的术语来表达复杂的系统和问题领域。以我最近的一次经历为例:我正在为一个书签管理工具编写原型,这只是我多年来构思的众多项目之一。我最初选择了 Haskell,因为我认为其数据建模之美和纯粹的无副作用推理会很有效:它速度快、优雅,而且一旦你用过 Parsec、Servant 和 optparse-applicative 等模块,就很难想象在没有它们的情况下编写某些东西(比如解析器)会是什么样子。

One of the steps in the proof-of-concept was transforming some data models to XML and output them to a file. If I were doing this in Kotlin or Java, it would be trivial: drop a dependency into Gradle, wire up Jackson or a standard DOM parser, and ten minutes later the data is in memory and ready to manipulate. After a frustrating hour with my Haskell project, and even after years of experience with the language, I was still wrestling with the dependencies, and later with monadic API, and I ended up giving up on the whole thing after I noticed I even forgot what I was doing in the first place. This has often been my friction point with Haskell. It is beautiful, but it fights you when you just want to get your hands dirty and prototype, even though type-driven development can also be nice and work well in some cases. 在概念验证的一个步骤中,我需要将一些数据模型转换为 XML 并输出到文件。如果是在 Kotlin 或 Java 中,这简直易如反掌:在 Gradle 中添加一个依赖,配置好 Jackson 或标准的 DOM 解析器,十分钟后数据就在内存中准备好供我操作了。但在我的 Haskell 项目中,经过令人沮丧的一小时,即便我已经有多年使用该语言的经验,我依然在与依赖项搏斗,随后又陷入了单子 API 的泥潭。最终,当我意识到自己甚至已经忘了最初要做什么时,我放弃了整个项目。这通常是我与 Haskell 产生摩擦的地方。它很美,但当你只想动手写原型时,它却在与你作对,尽管类型驱动开发在某些情况下确实不错且有效。

Scheme (GNU Guile for me) doesn’t have Haskell’s brutally efficient compiler, although it is quite speedy thanks to the C foundation. What it has is the terseness, power, and more importantly, it makes the actual act of hacking a joy. As elegant as Haskell’s purely functional foundation is, it can really complicate simple, crucial, impure tasks like writing to files or talking over a network. Monads are Haskell’s answer to this, but they often feel like a heavy abstraction tax; they allow you to write useful software, but they rarely make it intuitive or fast to prototype. These kind of heavy-handed abstractions are in my opinion really beautiful, but not justifiable for most projects. Please do ask yourself, do I really need a functional effect system, is it worth the complexity and cognitive load? Do I really need the pure/impure computation strictness enforced at compile time? Remember that later, just adding a simple print somewhere is not going to work without refactor (welcome to the IO monad). Scheme(我用的是 GNU Guile)没有 Haskell 那种极其高效的编译器,尽管得益于 C 语言基础,它也相当快。它拥有的是简洁、强大,更重要的是,它让编程本身变成了一种乐趣。尽管 Haskell 的纯函数式基础很优雅,但它确实会让一些简单、关键且不纯的任务(如写入文件或网络通信)变得复杂。单子是 Haskell 给出的解决方案,但它们往往感觉像是一种沉重的抽象税;它们确实能让你写出有用的软件,但很少能让原型开发变得直观或快速。在我看来,这种笨重的抽象虽然很美,但对于大多数项目来说并不合理。请问问自己:我真的需要一个函数式效应系统吗?它值得我付出如此高的复杂度和认知负荷吗?我真的需要在编译时强制执行纯/不纯计算的严格性吗?记住,以后如果你想在某处加一个简单的打印语句,不重构是不行的(欢迎来到 IO 单子的世界)。

As a long-time Lisper, for me this is a massive barrier to usability. In many ways, you can only fix what you can observe. Scheme happily sacrifices academic purity so you can slap a (write …) anywhere in your code and instantly see what’s going on. I’m sure a Haskell purist is burying their face in their hands right now, citing Debug.Trace or questioning why I’d want side-effects in a lazy, well-optimized language. They aren’t technically wrong, but the friction added to quick-and-dirty debugging is a tax I am simply not willing to pay when I’m trying to move fast. 作为一名资深的 Lisp 程序员,对我来说,这是可用性的巨大障碍。在很多方面,你只能修复你能观察到的东西。Scheme 乐于牺牲学术上的纯粹性,让你可以在代码的任何地方随手写上一句 (write ...),并立即看到发生了什么。我相信 Haskell 的纯粹主义者此刻一定正捂着脸,引用 Debug.Trace 或者质疑为什么我会在一门惰性且高度优化的语言中想要副作用。从技术上讲他们没错,但这种为快速调试增加的摩擦力,是我在追求快速开发时绝不愿支付的代价。

Meta-programming and DSLs

元编程与 DSL

The second problem with Monads is directly tied to their greatest strength: they are synonymous with Domain Specific Languages (DSLs). The promise of DSLs is fantastic—don’t write a complex program to solve a problem; write a simple program in a bespoke language designed solely for that task. Parsec is the golden child here; the parsing function is practically identical to the BNF grammar. But the success of Parsec has filled Hackage with hundreds of bespoke DSLs for everything. One for parsing, one for XML… 单子的第二个问题直接与其最大的优势挂钩:它们是领域特定语言(DSLs)的代名词。DSL 的愿景非常棒——不要为了解决问题而编写复杂的程序,而是用一种专门为该任务设计的定制语言编写一个简单的程序。Parsec 是这方面的典范;其解析函数几乎与 BNF 语法完全一致。但 Parsec 的成功导致 Hackage 上充斥着数百种针对各种事物的定制 DSL。一个用于解析,一个用于 XML……