(译) 理解 Elixir 中的宏 Macro, 第三部分:深入理解 AST
Elixir Macros 系列文章译文
- [1] (译) Understanding Elixir Macros, Part 1 Basics
- [2] (译) Understanding Elixir Macros, Part 2 - Macro Theory
- [3] (译) Understanding Elixir Macros, Part 3 - Getting into the AST
- [4] (译) Understanding Elixir Macros, Part 4 - Diving Deeper
- [5] (译) Understanding Elixir Macros, Part 5 - Reshaping the AST
- [6] (译) Understanding Elixir Macros, Part 6 - In-place Code Generation
原文 GitHub 仓库, 作者: Saša Jurić.
是时候继续探索 Elixir 的宏了. 上次我介绍了一些关于宏的基本原理, 今天, 我将进入一个较少谈及的领域, 并讨论Elixir AST 的一些细节.
跟踪函数调用
到目前为止, 你只看到了接受输入 AST 片段并将它们组合在一起的基础宏, 并在输入片段周围或之间添加了一些额外的样板代码. 由于我们不分析或解析输入的 AST, 这可能是最干净(或最不 hackiest)的宏编写风格, 这样的宏相当简单且容易理解.
然而, 有时候我们需要解析输入的 AST 片段以获取某些特殊信息. 一个简单的例子是 ExUnit 的断言. 例如, 表达式 assert 1+1 == 2+2 会出现这个错误:
Assertion with == failed
code: 1+1 == 2+2
lhs: 1
rhs: 2
这个宏 assert 接收了整个表达式 1+1 == 2+2, 然后从中分出独立的表达式用来比较, 如果整个表达式返回 false, 则打印它们对应的结果. 所以, 宏的代码必须想办法将输入的 AST 分解为几个部分并分别计算子表达式.
更多时候, 我们调用了更复杂的 AST 变换. 例如, 你可以借助 ExActor 这样做:
defcast inc(x), state: state, do: new_state(state + x)
它会被转换为大致如下的形
def inc(pid, x) do
:gen_server.cast(pid, {:inc, x})
end
def handle_cast({:inc, x}, state) do
{:noreply, state+x}
end
和 assert 一样, 宏 defcast 需要深入分析输入的 AST 片段, 并找出每个子片段(例如, 函数名, 每个参数). 然后, ExActor 会执行一个精巧的变换, 将各个部分重组成一个更加复杂的代码.
今天, 我将想你展示构建这类宏的基础技术, 我也会在之后的文章中会将变换做得更复杂. 但在此之前, 我要请你认真考虑一下你的代码是否有有必要基于宏. 尽管宏十分强大, 但也有缺点.
首先, 就像之前我们看到的那样, 比起那些 "普通" 的运行时抽象 (函数, 模块, 协议), 宏的代码会很快地变得非常多. 你可以依赖 undocumented format (译注: 缺少文档解释, 寓意代码极其难以理解) 的 AST 来快速完成许多嵌套的 quote/unquoted 调用, 以及奇怪的模式匹配.
此外, 宏的滥用可能使你的客户端代码 (译注: 使用宏的代码) 极其难懂, 因为它将依赖于自定义的非标准习惯用法(例如 ExActor 的 defcast). 这使得理解代码和了解底层究竟发生了什么变得更加困难.
从好的方面来看, 宏在删除样板代码时非常有用(正如 ExActor 示例所展示的那样), 并且具有访问运行时不可用的信息的能力(正如您应该从 assert 示例中看到的那样). 最后, 由于宏在编译期间运行, 因此可以通过将计算转移到编译时来优化一些代码.
因此, 肯定会有适合宏的情景, 您不应该害怕使用它们. 但是, 您不应该仅仅为了获得一些可爱的 dsl 式语法而选择宏. 在使用宏之前, 应该考虑是否可以依靠“标准”语言抽象(如函数、模块和协议)在运行时有效地解决问题.
探索 AST 结构
目前, 关于 AST 结构的文档不多. 然而, 在 shell 会话中可以很简单地探索和使用 AST, 我通常就是这样探索 AST 结构的.
例如, 这里有一个关于变量的 quoted
iex(1)> quote do my_var end
{:my_var, [if_undefined: :apply], Elixir}
在这里, 第一个元素代表变量的名称;第二个元素是上下文 Keyword 列表, 它包含了该 AST 片段的元数据(例如 imports 和 aliases). 通常你不会对上下文数据感兴趣;第三个元素通常代表 quoted 发生的模块, 同时也用于确保 quoted 变量的 hygienic. 如果该元素为 nil, 则该标识符是不 hygienic 的.
一个简单的表达式看起来包含了许多东西:
iex(2)> quote do a+b end
{:+, [context: Elixir, import: Kernel],
[{:a, [if_undefined: :apply], Elixir}, {:b, [if_undefined: :apply], Elixir}]}
看起来可能很复杂, 但是如果我向你展示更高层次的表达模式, 就很容易理解了:
{:+, context, [ast_for_a, ast_for_b]}
在我们的例子中, ast_for_a 和 ast_fot_b 遵循着你之前所看到的变量的形状(如 {:a, [if_undefined: :apply], Elixir}). 一般, quoted 的参数可以是任意复杂的, 因为它们描述了每个参数的表达式. 事实上, Elixir AST 是一个简单 quoted expression 的深层结构, 就像我给你展示的那样.
让我们看一个关于函数调用的例子:
iex(3)> quote do div(5,4) end
{:div, [context: Elixir, import: Kernel], [5, 4]}
这类似于 quoted + 的操作, 我们知道 + 实际上是一个函数. 事实上, 所有二进制运算符都会像函数调用一样被 quoted.
最后, 让我们来看一个被 quoted 的函数定义:
iex(4)> quote do def my_fun(arg1, arg2), do: :ok end
{:def, [context: Elixir, import: Kernel],
[
{:my_fun, [context: Elixir],
[
{:arg1, [if_undefined: :apply], Elixir},
{:arg2, [if_undefined: :apply], Elixir}
]},
[do: :ok]
]}
看起来有点吓人, 但可以只看重要的部分来简化它. 事实上, 这种深层结构相当于:
{:def, context, [fun_call, [do: body]]}
fun_call 是一个函数调用的结构(正如之前你看过的那样).
如你所见, AST 背后通常有一些逻辑和意义. 我不会在这里写出所有 AST 的形状, 但会在 iex 中尝试你感兴趣的简单的结构来探索 AST. 这是一个逆向工程, 但不是火箭科学.
写一个 assert 宏
为了快速演示, 让我们编写一个简化版的 assert 宏. 这是一个有趣的宏, 因为它重新定义了比较操作符的含义. 通常, 当你写下 a == b 表达式时, 你会得到一个布尔结果. 但是, 当将此表达式给 assert 宏时, 如果表达式的计算结果为 false, 则会打印详细的输出.
我将从简单的部分开始, 首先在宏里只支持 == 运算符. 可以知道, 我们调用 assert expected == required 时, 等同于调用 assert(expect == required), 这意味着我们的宏接收到一个表示比较的引用片段. 让我们来探索这个比较表达式的 AST 结果:
iex(1)> quote do 1 == 2 end
{:==, [context: Elixir, import: Kernel], [1, 2]}
iex(2)> quote do a == b end
{:==, [context: Elixir, import: Kernel],
[{:a, [if_undefined: :apply], Elixir}, {:b, [if_undefined: :apply], Elixir}]}
所以我们的结构本质上是 {:==, context, [quoted_lhs, quoted_rhs]}. 如果你记住了前几个系列中所演示的例子, 那么就不会感到意外, 因为我提到过二进制运算符是作为 2 个参数的函数被 quoted 的.
知道了 AST 的形状, 实现这个宏就很简单:
defmodule Assertions do
defmacro assert({:==, _, [lhs, rhs]} = expr) do
quote do
left = unquote(lhs)
right = unquote(rhs)
result = (left == right)
unless result do
IO.puts "Assertion with == failed"
IO.puts "code: #{unquote(Macro.to_string(expr))}"
IO.puts "lhs: #{left}"
IO.puts "rhs: #{right}"
end
result
end
end
end
第一个有趣的事情发生在第 2 行. 注意我们是如何对输入表达式进行模式匹配的, 希望它符合某种结构. 这完全没问题, 因为宏是函数, 这意味着您可以依赖于模式匹配、guards(守卫), 甚至有多子句宏. 在我们的例子中, 我们依靠模式匹配将(被 quoted 的)比较表达式的每一边带入相应的变量.
然后, 在 quoted 的代码中, 我们通过分别计算左边和右边重新解释 == 操作(第 4 行和第 5 行), 然后是整个结果(第 7 行). 最后, 如果结果为假, 我们打印详细信息(第 9-14 行).
来试一下:
iex(1)> defmodule Assertions do ... end
iex(2)> import Assertions
iex(3)> assert 1+1 == 2+2
Assertion with == failed
code: 1 + 1 == 2 + 2
lhs: 2
rhs: 4
false
将代码实现通用化
将之前的代码用到其他的运算操作符并不困难:
defmodule Assertions do
defmacro assert({operator, _, [lhs, rhs]} = expr)
when operator in [:==, :<, :>, :<=, :>=, :===, :=~, :!==, :!=, :in] do
quote do
left = unquote(lhs)
right = unquote(rhs)
result = unquote(operator)(left, right)
unless result do
IO.puts("Assertion with #{unquote(operator)} failed")
IO.puts("code: #{unquote(Macro.to_string(expr))}")
IO.puts("lhs: #{left}")
IO.puts("rhs: #{right}")
end
result
end
end
end
这里只有一点点变化. 首先, 在模式匹配中, 硬编码(hard code) :== 被变量 operator 取代了(第 2 行).
我还引入(实际上, 是从 Elixir 源代码中复制粘贴了)guard 语句指定了宏能处理的运算符集(第 3 行). 这个检查有一个特殊原因. 还记得我之前提到的, quoted a + b(或任何其它的二进制操作)的形状等同于引用 fun(a, b). 因此, 没有这些 guard 语句, 任何双参数的函数调用都会在我们的宏中结束, 这可能是我们不想要的. 使用这个 guard 语句能将输入限制在已知的二进制运算符中.
有趣的事情发生在第 9 行. 在这里我使用了 unquote(operator)(left, right) 来对操作符进行简单的泛型分派. 你可能认为我可以使用 left unquote(operator) right 来替代, 但它并不能运算. 原因是 operator 变量保存的是一个原子(如:==). 因此, 这个天真的 quoted 会产生 left :== right, 这甚至不符合 Elixir 的语法规定.
记住, 在 quote 时, 我们不组装字符串, 而是组装 AST 片段. 所以, 当我们想生成一个二进制操作代码时, 我们需要注入一个正确的 AST, 它(如前所述)与双参数的函数调用相同. 因此, 我们可以简单地使用函数调用的方式 unquote(operator)(left, right).
这一点讲完了, 今天的这一章也该结束了. 它有点短, 但略微复杂些. 下一章 《(译) Understanding Elixir Macros, Part 4 - Diving Deeper》, 我将深入 AST 解析的话题.
本文由博客群发一文多发等运营工具平台 OpenWrite 发布
(译) 理解 Elixir 中的宏 Macro, 第三部分:深入理解 AST的更多相关文章
- 深入理解.NET Core的基元(三) - 深入理解runtimeconfig.json
原文:Deep-dive into .NET Core primitives, part 3: runtimeconfig.json in depth 作者:Nate McMaster 译文:深入理解 ...
- C/C++ 中的宏/Macro
宏(Macro)本质上就是代码片段,通过别名来使用.在编译前的预处理中,宏会被替换为真实所指代的代码片段,即下图中 Preprocessor 处理的部分. C/C++ 代码编译过程 - 图片来自 nt ...
- C#中的深度学习(三):理解神经网络结构
在这篇文章中,我们将回顾监督机器学习的基础知识,以及训练和验证阶段包括哪些内容. 在这里,我们将为不了解AI的读者介绍机器学习(ML)的基础知识,并且我们将描述在监督机器学习模型中的训练和验证步骤. ...
- 简单理解ECMAScript2015中的箭头函数新特性
箭头函数(Arrow functions),是ECMAScript2015中新加的特性,它的产生,主要有以下两个原因:一是使得函数表达式(匿名函数)有更简洁的语法,二是它拥有词法作用域的this值,也 ...
- 理解 JavaScript 中的 this
前言 理解this是我们要深入理解 JavaScript 中必不可少的一个步骤,同时只有理解了 this,你才能更加清晰地写出与自己预期一致的 JavaScript 代码. 本文是这系列的第三篇,往期 ...
- uboot中的中断macro宏
目录 uboot中的中断macro宏 引入 内存分配 流程概览 普通中断 保存现场 中断函数打印具体寄存器 恢复现场 软中断 空间获取 保存现场 附录速记 疑惑待解 title: uboot中的中断m ...
- [译]线程生命周期-理解Java中的线程状态
线程生命周期-理解Java中的线程状态 在多线程编程环境下,理解线程生命周期和线程状态非常重要. 在上一篇教程中,我们已经学习了如何创建java线程:实现Runnable接口或者成为Thread的子类 ...
- [NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()
译者注: 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正. 文末会有几个小问题,大家不妨一起思考一下 ...
- 【译】理解Rust中的闭包
原文标题:Understanding Closures in Rust 原文链接:https://medium.com/swlh/understanding-closures-in-rust-21f2 ...
- [易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro]
[易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro] 实用知识 宏Macro 我们今天来讲讲Rust中强大的宏Macro. Rust的宏macro是实现元编程的强大工具. ...
随机推荐
- TVM: 编译流程
深度学习编译器介绍 每一种硬件对应一门特定的编程语言,再通过特定的编译器去进行编译产生机器码,那随着硬件和语言的增多,编译器的维护难度会有很大困难.现代编译器已经解决了这个问题. 为了解决这个问题,科 ...
- Python中strftime()与strptime()的行为与datetime的时间格式码
前言 datetime在python中的作用不可小视,它可以与string进行相互转化,比如 import datetime # 将输出当前时间的'日/月/年' datetime.datetime.n ...
- c语言笔记(翁凯男神
哼,要记得好好学习去泡帅哥吖 一.快速入门 %p 输出地址 #include <stdio.h> void f(int *p); int main(){ int i = 1; printf ...
- AtCoder Beginner Contest 372 补题记录
A - delete 题意: 输出删除字符串中 . 后的字符串 思路: 只输出字符串中不是 . 的字符 void solve() { string s = sread(); for(auto it:s ...
- css样式修改-悬浮数字
代码实现 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF- ...
- 记录一次自己用 AI 写IOS APP的经历
我是几乎没有移动端开发经验的.仅有的一点安卓开发经验还是十几年前没毕业的时候自己瞎折腾. 故事的起源是每天辅导我儿子功课时的暴跳如雷. 我儿子上一年级了,在语文的生词上落后得非常严重(当然可能是他同学 ...
- mysql页中的行记录头部信息
mysql中具体的数据是存储在行中的,而行是存储在页中的.也就是说页是凌驾于行之上的. mysq一个页大小为16K,当然这个大小是可以通过修改配置文件来改变的. mysql页结构大致示意图: 当我们新 ...
- 【附源码】C语言的学生管理系统完整实现方案
以下是一个基于C语言的学生管理系统完整实现方案,结合了结构体.链表.文件存储.菜单驱动等核心技术,参考了多个开源项目与课程设计案例. 系统支持管理员/学生双角色权限.数据持久化存储及完整增删改查功能, ...
- [gym103860D]Tree Partition
D - Tree Partition 考虑将树转换到一个序列上,钦定\(1\)为根节点,\(1\)的父亲为\(0\),在序列上,孩子向父亲连边 然后考虑设\(dp\)状态\(dp[i][j]\)表示前 ...
- 适用于编程小白的Python学习01:Pandas初探
什么是Python虚拟环境? Python虚拟环境是一个独立的.隔离的Python运行环境,它允许你为每个项目安装独立的库和依赖项,而不会与系统中其他Python项目或全局Python环境发生冲突,从 ...