返回资讯中心
外部精选
软件工程
#开发者实时流

Project Steadfast:通过操作 Ruby AST 实现元编程

来源标题:Project Steadfast

Project Steadfast 提出用操作 Ruby AST 的方式实现元编程,以替代 Ruby 中常见的 eval 字符串拼接手法。eval 虽被 ERB、Rails 关联与脚手架等功能用于提升性能,但在高度动态场景下易出错、受限且可能遭受注入攻击。作者以自研 Papercraft 的 DSL 为例,说明手工拼接 Ruby 源码字符串的繁琐。

Hacker News 最新ciconia2026/11/8 · 今天15 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

最近我写过一个在我脑海中萦绕已久的想法——在 Ruby 中通过操作 Ruby AST 来实现元编程,类似于 Lisp 或 Elixir 等语言的宏能力。使用 eval(及其各种变体)是 Ruby 中一种常见的元编程技术,其做法是在运行时从动态构造的字符串生成代码。这样做通常是出于性能考虑:ERB 通过生成一段段 Ruby 源代码再拼接起来并调用 eval,将模板编译为优化后的代码;在 Rails 中,同样的技术被用于实现关联(associations)、脚手架(scaffolding)等功能。借助 eval,我们可以避免使用 Ruby 中的 DSL 时常常伴随的性能开销。

但在高度动态的上下文中使用 eval 也可能容易出错、功能受限,并且有可能遭受注入攻击。不得不费尽心思地构造包含 Ruby 源代码的字符串可能是一项艰巨的任务,尤其是当你想要把 DSL 编译成高效的代码时,就像 ERB 或我自己的 Papercraft 那样。

在 Papercraft 中,HTML 是使用 DSL 生成的。下面是一个例子: header { p { strong(repo_name) } p(repo_description) p { code(clone_command) }

虽然这种代码可以借助 builder 模式来生成 HTML,而这也是 Ruby 中实现 DSL 的传统方式,但它会带来显著的性能开销。这就是为什么在 Papercraft 中我选择实现一个编译器,把上面的代码编译成优化后的形式: buffer .<<("<header><p><strong>").<<(ERB::Escape.html_escape((repo_name))) .<<("</strong></p><p>").<<(ERB::Escape.html_escape((repo_description))) .<<("</p><p><code>").<<(ERB::Escape.html_escape((clone_command))) .<<("</code></p></header>")

这确实解决了性能问题(与 ERB 模板相当),但它引入了另一个问题,即 Papercraft 编译器代码本身的复杂性。我对 Papercraft 编译器的代码并不特别自豪,其复杂性源于这类优化代码是动态生成的,必须由许多小块拼凑而成。这就是为什么我长期以来一直在思考如何简化它。

最终我想到,一种使用通用宏展开技术来生成代码的工具——例如 Lisp 或 Elixir 等语言中的 quote/unquote 机制——可能非常适合解决这类问题。这样的工具不仅可以用于在运行时生成新代码,还可以用于在运行时操作和修改现有代码。

作为这种技术如何应用的一个非常简单的例子,让我们实现一个通用的校验器生成器。它的工作是生成一个校验器 lambda,使用 === 运算符(这是 Ruby 中用于匹配 case 子句的运算符)根据一个或多个条件校验给定的值。我们可以直接生成一个闭包并遍历这些条件: def create_validator(*conds) ->(v) { conds.each { raise unless it === v } v end

这相当简洁明了,但如果我们想从中榨取更多性能,可能希望展开那个 conds.each 循环,这可以通过插入一段包含展开后代码的字符串,然后将其传给 eval 来实现: def create_validator(*conds) code = <<~RUBY ->(v) { #{conds.map { "raise unless (#{it.inspect}) === v;" }.join} RUBY eval(code) end

这完全可行,但请注意,我们不得不把嵌入的代码嵌套在所要构造的字符串里面,这有点笨拙。借助 quote/unquote 工具——它让我们能用 Ruby 代码构造 Ruby 代码——我们可以这样做: def create_validator(*conds) ast = quote { ->(v) { unquote conds.map { |c| quote { unquote(c) === v } } v eval(ast.to_source) end

用 #quote 我们把代码放进引号中(显然),用 #unquote 我们暂时跳出引号,以便执行一些计算并将结果注入回被引用的代码中。在这个例子中,我们通过调用 quote { unquote(c) === v } 把每个条件转换成一段 Ruby AST。

在我的文章 Unrolled Code 中,我探讨了这种技术如何应用于许多不同的问题。这就引出了 Steadfast 项目。

我很高兴地宣布,我已被选为 Ruby Association 资助的获得者,将使用 quote/unquote 技术开发一个 Ruby 通用宏工具,并由 Matz 担任我的导师。

该项目的范围包括开发 quote/unquote 工具,以及用于生成和操作 Prism AST、将 AST 转换为源代码等的附加工具。项目还将探索这些工具在 ERB、ActiveRecord、Roda 等流行 gem 中的应用。我想看看我能把这些想法推进到多远,它们如何集成到 Ruby 运行时中,以及这种技术的局限性和缺点是什么。

我将在我创建的一个名为 Steadfast 的新仓库中开展这项工作。如果你愿意,可以看看我画的一些草图,它们展示了 quote/unquote 的可能用法。Steadfast 是我在 Sirop 上工作的延续,Sirop 是我为将 Prism AST 转换为源代码而构建的工具,目前被 Papercraft 编译器使用。

Steadfast 献给那片土地上勇敢的人们,他们在难以想象的暴力、迫害、冷酷和漠视面前坚持不懈。

在接下来的四个月里,我将分享这个项目的进展消息,该项目预计于 2027 年 3 月下旬完成。我认为这个项目中引入的新技术可以带来一种在 Ruby 中进行元编程的新方式。希望其他人会觉得这项工作有用,并为 Steadfast 想出新的用途。

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →