Lisp 对 Ruby 的影响

Hacker News Top 新闻

摘要

一篇技术博客文章,探讨 Ruby 的设计(包括闭包、一等函数、符号和方法命名约定)如何受到 Lisp 的影响。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/06/15 00:58

# Lisp 对 Ruby 的影响 来源:https://blog.tacoda.dev/lisps-influence-on-ruby-6a54f1a7740e?gi=0730addad8f2 伊恩·约翰逊 (https://blog.tacoda.dev/?source=post_page---byline--6a54f1a7740e---------------------------------------) 有一次我写下了 `users\.select \{ \|u\| u\.admin? \}\.map\(&:email\)`,然后意识到我写的是 Lisp。 不是字面上的 Lisp。括号不见了,前缀表示法不见了,lambda 变成了语法块。但代码的形状(把一个过滤器链到变换上,用 `?` 对每个元素问一个是否问题,不修改任何东西地构建结果)是 Lisp 的。Ruby 只是给它换上了商务休闲装。 Matz 也这样说过。他把 Ruby 的设计描述为从一个简单的 Lisp 开始,剥离掉宏和 S 表达式,然后加入对象系统、块和 Smalltalk 风格的方法。大多数 Ruby 爱好者爱上的不是面向对象的特性,而是那些披着友好外衣的函数式特性。 下面是我经常想到的列表,以及每一项为什么重要。 ## 带问号的方法名 谓词以 `?` 结尾的惯例来自 Scheme。`zero?`、`nil?`、`empty?`、`respond\_to?`、`valid?`。这个标记一眼就告诉你:这个方法回答一个是否问题。它不会修改,不会执行动作,它告诉你关于接收者的某个真或假的信息。 ``` return if user.nil? return unless user.admin? notify(user) if user.subscribed? ``` 你可以把这三行当作英语来读,因为 `?` 做了大部分工作。同样的惯例也出现在 `!` 上,用于会修改或抛出异常的方法:`save!`、`sort!`、`compact!`。这两个标记都来自 Scheme,其中 `null?`、`pair?` 和 `set!` 的工作方式相同。 一个小的语法借用,却贯穿了整个语言。因为这两个字符,阅读 Ruby 更快了。 ## 闭包和块 当被问到喜欢 Ruby 的哪一点时,大多数 Ruby 开发者最先提到的特性就是块。它们是闭包:捕获周围作用域并可以作为值传递的代码块。 ``` total = 0 [1, 2, 3].each { |n| total += n } total # => 6 ``` 这个块闭包了 `total`。这就是闭包模式:一个函数值记住了它被定义时的环境。Lisp 比 Ruby 早几十年就有了闭包。Scheme 让它们成为可以传递给任何东西的一等对象。Ruby 保留了这一思想,并加入了更轻量的语法。一个块——用 `do...end` 或花括号——是一个去掉了括号的闭包。 Proc 和 lambda 是同样的思想,只是加回了括号: ``` square = ->(n) { n * n } [1, 2, 3].map(&square) # => [1, 4, 9] ``` 那个箭头语法是 Ruby 的 `lambda`。这个词本身来自 Lisp,源自 Church 的 λ 演算,并在 1958 年首次被应用到一种可工作的编程语言中。 ## 一等函数 一旦你可以命名一个闭包并传递它,函数就成了值。你可以把它们存储在数组中、从方法中返回、或附加到对象上。Ruby 的 `Method` 和 `Proc` 类使这一点显式化。`&:method\_name` 也是如此,它通过查找接收者上的方法,将一个符号转换为一个块。 ``` emails = users.map(&:email) admins = users.select(&:admin?) ``` 这个 `&:foo` 是一点小魔法,它之所以有效,是因为在 Ruby 中函数是值。符号被强制转换为 proc,proc 作为块传递,然后在每个元素上调用该块。一等函数贯穿始终。 这是 Lisp 的基本思想:程序由函数组合构建。Ruby 借用了这种组合,并用点链来装饰它。 ## 符号 `:foo` 是一个符号。它看起来像一个带冒号的字符串,但它是另一种类型的值。符号是 interned 的:每次你写 `:foo`,你得到的是同一个对象。两个看起来相同的字符串通常内存中是两个不同的对象;而两个看起来相同的符号永远是一个。 这个性质来自 Lisp。Lisp 符号(在某些方言中称为原子)是最早的 interned 值。读取器看到 `foo`,在符号表中查找它,要么返回现有的符号,要么创建一个新的并记住它。之后,所有对 `foo` 的引用都指向同一个对象。 ``` :status.equal?(:status) # => true "status".equal?("status") # => false ``` 它在 Ruby 中带来的好处:快速比较、免费哈希、以及为不是字符串的名字提供的干净语法。 ``` config = { host: "localhost", port: 5432, ssl: true } config[:host] ``` 散列键是明显的用例,但更深层的用途是方法名。`method\_name` 和 `:method\_name` 在两层上是同一个思想。`send(:save)` 调用 `save` 方法。`define\_method(:fetch) \{...\}` 定义一个方法。`respond\_to?(:to\_s)` 询问是否存在一个方法。符号是 Ruby 反射式引用方法的方式,这也是元编程的工作方式。 上一节中的 `&:foo` 快捷方式也是同样的思想,只是更近一步:一个命名方法的符号,被强制转换为可调用对象。符号承载名字;Ruby 查找它们。 ## 集合方法 `map`、`select`、`reject`、`reduce`、`each`、`flat\_map`、`zip`、`partition`、`chunk\_while`。`Enumerable` 模块是我如果必须离开 Ruby 最会想念的部分。它也是最直接继承自 Lisp 的部分。 Lisp 给了我们 `mapcar`、`filter`、`reduce`。结构是相同的:取一个集合,应用一个函数,返回一个集合。没有索引,没有差一错误,没有忘记重置的累加器变量。 ``` orders .select { |o| o.placed_at > 1.week.ago } .group_by(&:customer_id) .transform_values { |group| group.sum(&:total) } ``` 在一种表达能力较弱的语言中,那段代码需要五个 for 循环和一个散列。而在 Ruby 中,它是一个从上到下阅读的段落。这个链做的事情,与一系列嵌套的 Lisp `map` 和 `reduce` 做的相同;只是语法从括号变成了点号。 ## 获取伊恩·约翰逊的故事,直接发送到您的收件箱 免费加入 Medium,即可收到来自这位作者的最新更新。 记住我,以便更快登录 当 Ruby 开发者说“这种语言读起来像英语”时,通常他们指的是“集合方法组合成了句子”。那是 Lisp 的馈赠,配上 Ruby 的标点符号。 ## 惰性枚举器 急切的集合方法会构建整个结果,然后返回它。`[1, 2, 3].map \{ |n| n * 2 \}` 分配一个新数组,填充它,然后交回来。对于小列表还可以;对于大型或无限列表来说就是个问题。 Lisp 用惰性求值和流解决了这个问题。Scheme 的 `delay` 和 `force`,Clojure 的惰性序列,Haskell 的一切。其思想是:直到有人请求时才计算结果。列表不是内存里的一个数组;它是一个一次产生一个元素的配方。 Ruby 也有同样的技巧。`Enumerable\#lazy` 返回一个枚举器,它将操作管道化,而不生成中间集合。 ``` (1..Float::INFINITY) .lazy .select { |n| n % 3 == 0 } .map { |n| n * n } .first(5) # => [9, 36, 81, 144, 225] ``` 这个管道从一个无限范围读取。没有 `lazy` 的话,`select` 会尝试扫描整个范围再传递下去;程序永远不会结束。有了 `lazy`,每个值一次一个地流过链,而且只有五个值被计算出来。 其机制是纯 Lisp 的。一个惰性枚举器是一个对源加上一个变换的闭包。调用 `next` 将闭包前进一步。`first(5)` 调用 `next` 五次,然后停止。其他所有东西都保持未计算状态。 你不经常用到它。但当你需要时(分页读取大文件、生成组合直到找到一个合适的、遍历树而不展平它),Ruby 中没有其他东西能像它一样干净地完成工作。 ## 鸭子类型 如果它走路像鸭子,叫声像鸭子,就把它当鸭子对待。不要检查它的类型。给它发送消息,看看会发生什么。 Smalltalk 在这里也有功劳。Smalltalk 的“向任何对象发送任何消息”比 Lisp 的类型静态但动态方法更接近鸭子类型。但 Lisp 的动态类型传统——值知道它们的类型,而变量不知道——也是同一渊源的一部分。函数应该关心行为而不是类的思想贯穿两者。 ``` def render(thing) thing.to_s end ``` 这个方法对任何响应 `to\_s` 的东西都有效:字符串、整数、自定义对象、`nil`。这个方法不问 `thing` *是*什么,它问 `thing` 能*做*什么。这种姿态(行为胜于身份)是 Ruby 让人觉得宽容的部分原因。 ## 表达式导向的设计 Ruby 中的每个语句都返回一个值。`if` 返回一个值。`case` 返回一个值。方法返回它的最后一个表达式。块返回它的最后一个表达式。 ``` status = case response.code when 200..299 then :ok when 400..499 then :client_error when 500..599 then :server_error else :unknown end ``` 那是 Lisp。Lisp 没有语句,只有表达式。每种形式都会求值出某个东西。Ruby 保留了这种纪律,但没有保留括号,结果就是代码可以组合。你可以把任何表达式放在任何位置上。 有语句的语言要求你写额外的行。`if (x) { result = a; } else { result = b; }` 本来一行就能完成,却写了三行。Ruby 和 Lisp 都拒绝这种分割。`result = if x then a else b end`。少了一个变量,少了一个可能忘记的赋值。 ## 写代码的代码 Lisp 的标志性技巧是代码即数据。程序是列表,列表是值,所以一个程序可以接收一个程序并返回一个程序。宏——Lisp 被模仿最多却最难以复制的特性——是在代码运行前对其进行操作的函数。 Ruby 没有宏。它拥有次优选择:一个允许你在运行时重塑类的元对象协议。`define\_method`、`method\_missing`、`class\_eval`、`instance\_eval`、开放类。没有一样像 Lisp 的宏那样优雅,但所有这些东西都解决同一类问题。 ``` class Status %i[draft published archived].each do |state| define_method("#{state}?") do @state == state end end end ``` 这段代码在类定义时生成了三个谓词方法。在缺乏一等元编程的语言中,你需要手动编写三个方法并接受重复。你可以写一个循环来定义方法这一事实,直接源自“代码即数据”。这是同一个思想,更窄一点,是在一门用块取代了宏的语言中。 这就是 Ruby 中 DSL 很容易写的原因:RSpec、Rails 路由、Rake、Sinatra。它们看起来像英语,因为 Ruby 的语法可以弯曲。它们之所以能弯曲,是因为底层模型更接近 Lisp 而不是 C。你越仔细看一个 Ruby DSL,就看到越多的方法调用层层深入:接收者和消息像 Smalltalk,而元编程像 Lisp 一样雕刻形状。 ## 为什么 FP 和 OOP 不是一场战斗 你可能会忍不住把上述所有内容解读为“Ruby 其实是一门函数式语言”。但它不是。Ruby 是一门带有函数式口音的面向对象语言,而大部分乐趣就来自这个口音。 函数式与面向对象的争论基本上是一个范畴错误。两种范式回答的是不同的问题。OOP 选择一个抽象(通常是领域名词,一个有状态和行为的事物)并从那里开始构建。FP 选择另一个抽象(一个函数,一个变换,一个组合)并从那里开始构建。选择在于哪个抽象被放在中心。 Ruby 选择了对象。然后它允许你在对象上调用 `map`。 你可以整天在 Ruby 中写函数式代码。`users.map(&:email).reject(&:empty?).sort.uniq` 是纯粹的函数式管道。没有修改,没有共享状态,没有意外。你也可以写深度面向对象的 Ruby:领域模型、ActiveRecord、服务对象、依赖注入。两种风格放在同一个文件里。有时甚至放在同一行。 Lisp 早就经历过这个对话。Common Lisp 对象系统是有史以来最强大的 OO 系统之一,它位于一个通常被称为函数式的语言内部。Scheme 在你需要的时候也有对象;它们是带有一个调度表的闭包。两种范式从来都是兼容的。它们之间的敌意只是我们讲给自己听的故事。 重要的是主抽象。选择适合问题的那个。如果领域里充满了带状态的行为,那就以对象为主,并用函数式方法来操作这些对象的集合。如果领域是一个变换管道,那就以函数为主,并用对象在管道中携带数据。Ruby 同时支持两者,因为 Lisp 和 Smalltalk 两者都同时支持,而 Ruby 正是 Matz 通过从它们各取所长而构建的语言。 ## 相同的形状,不同的油漆 人们喜爱的 Ruby 的表现力并非 Ruby 原创。它是对更早语言的精心选择,其中 Lisp 是最大的单一来源。了解这些想法的来源,有助于更有意地使用它们,也使得学习下一门语言更容易,因为同样的想法会再次出现在 Clojure、Elixir、Scheme、OCaml 中。相同的形状,不同的油漆。

相似文章

Rust类型系统中的Lisp

Hacker News Top

一个嵌入在Rust trait系统中的Lisp解释器,支持在编译时进行递归函数、闭包和延续传递风格。

Lisp在网页应用中的运用(2001)

Hacker News Top

Paul Graham结合自己创办Viaweb的经验,讨论了在网页应用中使用Lisp的优势,包括语言自由度、增量开发以及快速修复bug。

为什么多年来 Ruby 依然让人有家的感觉

Lobsters Hottest

作者回顾了使用 Ruby 的 15 年经历,称赞了其隐藏特性,如 refinements、delegation 以及新的 ZJIT JIT 编译器,并指出 Ruby 搭配 ZJIT 正在缩小与 Go 和 Rust 等更快语言的性能差距。