(译) 理解 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ć.
上次我介绍了一个基本版本的可追溯宏 deftraceable
, 它允许我们编写可跟踪的函数. 这个宏的最终版本还有一些遗留的问题, 今天我们将解决其中一个 — 参数模式匹配.
从今天的练习应该认识到, 我们必须仔细考虑关于宏可能接收到的输入的所有假设情况.
问题所在
正如我上次所暗示的那样, 当前版本的 deftraceable
不能使用模式匹配的参数. 让我们来演示一下这个问题:
iex(1)> defmodule Tracer do ... end
iex(2)> defmodule Test do
import Tracer
deftraceable div(_, 0), do: :error
end
** (CompileError) iex:5: unbound variable _
发生了什么? deftraceable
宏盲目地假设输入参数是普通变量或常量. 因此, 当你调用 deftracable div(a, b)
时, deftracable div(a, b), do: ...
生成的代码将包含:
passed_args = [a, b] |> Enum.map(&inspect/1) |> Enum.join(",")
上面这段会按预期工作, 但如果一个参数是匿名变量(_
), 那么我们将生成以下代码:
passed_args = [_, 0] |> Enum.map(&inspect/1) |> Enum.join(",")
这显然是不正确的, 因此我们得到了未绑定变量错误.
那么解决方案是什么呢? 我们不应该对输入参数做任何假设. 相反, 我们应该将每个参数放入宏生成的专用变量中. 或者用代码来表达, 如果宏被调用:
deftraceable fun(pattern1, pattern2, ...)
我们会生成这样的函数头:
def fun(pattern1 = arg1, pattern2 = arg2, ...)
这将允许我们将参数值代入内部临时变量, 并打印这些变量的内容.
解决方案
让我们来实现它. 首先, 我将向你展示解决方案的顶层示意版:
defmacro deftraceable(head, body) do
{fun_name, args_ast} = name_and_args(head)
# 通过给每个参数添加 "= argX"来装饰输入参数.
# 返回参数名称列表 (arg1, arg2, ...)
{arg_names, decorated_args} = decorate_args(args_ast)
head = ?? # Replace original args with decorated ones
quote do
def unquote(head) do
... # 不变
# 使用临时变量构造追踪信息
passed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",")
... # 不变
end
end
end
首先, 我们从函数头(head)提取函数名称和 args (我们在前一篇文章中解决了这个问题). 然后, 我们必须将 = argX
注入到 args_ast
中, 并收回修改后的参数(我们将将其放入 decorated_args
中).
我们还需要生成的变量的纯名称(或者更确切地说是它们的 AST), 因为我们将使用这些名称来收集参数值. 变量 arg_names
实际上包含 quote do [arg_1, arg_2, ....] end
, 可以很容易地注入到 AST 树中.
我们来实现剩下的部分. 首先, 让我们看看如何修饰参数:
defp decorate_args(args_ast) do
for {arg_ast, index} <- Enum.with_index(args_ast) do
# 动态生成 quoted 标识符
arg_name = Macro.var(:"arg#{index}", __MODULE__)
# 为 patternX = argX 生成 AST
full_arg = quote do
unquote(arg_ast) = unquote(arg_name)
end
{arg_name, full_arg}
end
|> Enum.unzip
end
大多数操作发生在 for
语句中. 本质上, 我们处理了每个变量输入的 AST 片段, 然后使用 Macro.var/2
函数计算临时名称(quoted 的 argX
), 它能将一个原子变换成一个名称与其相同的 quoted 的变量. Macro.var/2
的第二个参数确保变量是hygienic 的. 尽管我们将 arg1, arg2, ...
变量注入到调用者上下文中, 但调用者不会看到这些变量. 事实上, deftraceable
的用户可以自由地使用这些名称作为一些局部变量, 不会干扰我们的宏引入的临时变量.
最后, 在推导式的末尾, 我们返回一个元组, 该元组由临时的名称和 quoted 的完整模式组成 - (例如 _ = arg1
, 或 0 = arg2
). 使用 unzip
和 to_tuple
进行推导之后确保 decorate_args
以 {arg_names, decorated_args}
的形式返回结果.
decorate_args
辅助变量就绪后, 我们就可以传递输入参数, 并获得修饰参数, 以及临时变量的名称. 现在我们需要将这些修饰过的参数注入到函数的头部, 以取代原始参数. 要注意, 我们需要做到以下几点:
- 递归遍历输入函数头的 AST
- 找到指定函数名和参数的位置
- 用修饰过的参数的 AST 替换原始(输入)参数
如果我们使用宏, Macro.postwalk/2
这个处理可以被合理地简化掉:
defmacro deftraceable(head, body) do
{fun_name, args_ast} = name_and_args(head)
{arg_names, decorated_args} = decorate_args(args_ast)
# 1. 递归地遍历 AST
head = Macro.postwalk(
head,
# lambda 函数处理输入 AST 中的元素, 返回修改过的 AST
fn
# 2. 模式匹配函数名和参数所在的位置
({fun_ast, context, old_args}) when (
fun_ast == fun_name and old_args == args_ast
) ->
# 3. 将输入参数替换为修饰参数的 AST
{fun_ast, context, decorated_args}
# 头部 AST 中的其它元素(可能是 guards)
# -> 我们让它保留不变
(other) -> other
end
)
... # 不变
end
Macro.postwalk/2
递归地遍历 AST, 并且在所有节点的后代被访问之后, 为每个节点调用提供的 lambda 函数. lambda 函数接收元素的 AST, 这样我们有机会返回一些除了指定节点之外的东西.
我们在这个 lambda 里做的实际上是一个模式匹配, 我们在寻找 {fun_name, context, args}
. 如第三篇文章中所述那样, 这是表达式 some_fun(arg1, arg2, ...)
的 quoted 表现形式. 一旦我们遇到匹配此模式的节点, 我们只需要用新的(修饰过的)输入参数替换掉旧的. 在所有其它情况下, 我们简单地返回输入的 AST, 使得树的其余部分不变.
这看着有点复杂了, 但它解决了我们的问题. 以下是 deftraceable 宏的最终版本:
defmodule Tracer do
defmacro deftraceable(head, body) do
{fun_name, args_ast} = name_and_args(head)
{arg_names, decorated_args} = decorate_args(args_ast)
head = Macro.postwalk(head,
fn
({fun_ast, context, old_args}) when (
fun_ast == fun_name and old_args == args_ast
) ->
{fun_ast, context, decorated_args}
(other) -> other
end)
quote do
def unquote(head) do
file = __ENV__.file
line = __ENV__.line
module = __ENV__.module
function_name = unquote(fun_name)
passed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",")
result = unquote(body[:do])
loc = "#{file}(line #{line})"
call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
IO.puts "#{loc} #{call}"
result
end
end
end
defp name_and_args({:when, _, [short_head | _]}) do
name_and_args(short_head)
end
defp name_and_args(short_head) do
Macro.decompose_call(short_head)
end
defp decorate_args([]), do: {[],[]}
defp decorate_args(args_ast) do
for {arg_ast, index} <- Enum.with_index(args_ast) do
# 动态生成 quoted 标识符(identifier)
arg_name = Macro.var(:"arg#{index}", __MODULE__)
# 为 patternX = argX 构建 AST
full_arg = quote do
unquote(arg_ast) = unquote(arg_name)
end
{arg_name, full_arg}
end
|> Enum.unzip
end
end
让我们来试试:
iex(1)> defmodule Tracer do ... end
iex(2)> defmodule Test do
import Tracer
deftraceable div(_, 0), do: :error
deftraceable div(a, b), do: a/b
end
iex(3)> Test.div(5, 2)
iex(line 6) Elixir.Test.div(5,2) = 2.5
iex(4)> Test.div(5, 0)
iex(line 5) Elixir.Test.div(5,0) = :error
正如你所看到的那样, 可以进入 AST, 分解它, 并在其中散布一些自定义的注入代码, 这并不算很复杂. 缺点是, 编写的宏的代码会变得越来越复杂, 并且更难分析.
今天的话题到此结束. 下一次, 我将讨论原地代码生成技术 《(译) Understanding Elixir Macros, Part 6 - In-place Code Generation》.
本文由博客群发一文多发等运营工具平台 OpenWrite 发布
(译) 理解 Elixir 中的宏 Macro, 第五部分:组装 AST的更多相关文章
- [NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()
译者注: 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正. 文末会有几个小问题,大家不妨一起思考一下 ...
- C/C++ 中的宏/Macro
宏(Macro)本质上就是代码片段,通过别名来使用.在编译前的预处理中,宏会被替换为真实所指代的代码片段,即下图中 Preprocessor 处理的部分. C/C++ 代码编译过程 - 图片来自 nt ...
- 【译】Surface中你也许不知道的五件事
Bring up the Quick Link Menu - Select the Windows Key + X or right click the Start Button to bring u ...
- uboot中的中断macro宏
目录 uboot中的中断macro宏 引入 内存分配 流程概览 普通中断 保存现场 中断函数打印具体寄存器 恢复现场 软中断 空间获取 保存现场 附录速记 疑惑待解 title: uboot中的中断m ...
- [译]线程生命周期-理解Java中的线程状态
线程生命周期-理解Java中的线程状态 在多线程编程环境下,理解线程生命周期和线程状态非常重要. 在上一篇教程中,我们已经学习了如何创建java线程:实现Runnable接口或者成为Thread的子类 ...
- 【译】理解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是实现元编程的强大工具. ...
- C中的宏
1. 简单宏定义 简单的宏定义有如下格式: [#define指令(简单的宏)] #define 标识符替换列表 替换列表是一系列的C语言记号,包括标识符.关键字.数.字符常量.字符串字面量.运算符和 ...
- Flask基础(15)-->模板代码的复用【宏(Macro)、继承(Block)、包含(include)】
宏 对宏(macro)的理解: 把它看作 Jinja2 中的一个函数,它会返回一个模板或者 HTML 字符串 为了避免反复地编写同样的模板代码,出现代码冗余,可以把他们写成函数以进行重用 需要在多处重 ...
- 深入理解php中的ini配置(1)
这篇文章不会详细叙述某个ini配置项的用途,这些在手册上已经讲解的面面俱到.我只是想从某个特定的角度去挖掘php的实现机制,会涉及到一些php内核方面的知识:-) 使用php的同学都知道php.ini ...
随机推荐
- 操作系统综合题之“按要求是个进程协调完成任务,补充完整下列程序,将编号①~⑩处空缺的内容填写(Buffer缓冲区问题-代码补充)”
1.问题:假设某系统有四个进程.input1和input2进程负责从不同设备读取数据,分别表示为data1和data2,存放在缓冲区Buffer中,output1和output2进程负责从Buffer ...
- 仿EXCEL插件,智表ZCELL产品V1.8 版本发布,增加行高鼠标拖动功能
详细请移步 智表(ZCELL)官网www.zcell.net 更新说明 这次更新主要应用户要求,主要增加了行高鼠标拖动功能,并增加了单元格换行等功能,欢迎大家体验使用. 本次版本更新内容如下: 版本 ...
- Ubuntu20.04 搭建Kubernetes 1.28版本集群
环境依赖 以下操作,无特殊说明,所有节点都需要执行 安装 ssh 服务 安装 openssh-server sudo apt-get install openssh-server 修改配置文件 vim ...
- ubuntu 踩过的坑
ubuntu安装中文输入法成功教程: https://zhuanlan.zhihu.com/p/508797663 博主希望尽量的不去宿主机中操作,达到对原系统的保护的效果,并且能够进行日常的深度学习 ...
- PB EB ZB YB
1B字节=8bit位 1KB=2^10B 1MB=2^20B 1GB=2^30B 1TB=2^40B 1PB=2^50B(五个屁) 1EB=2^60B(六姨) 1ZB=2^70B(七个乌贼) 1YB= ...
- C#LINQ去掉数组字符串中的指定元素
例字符串: string s1 = "111,111,111222111,111333111,111"; string del = "111"; 要删除指定元素 ...
- python里的简洁操作
1.lambda匿名函数好处 精简代码,lambda省去了定义函数,map省去了写for循环过程:res=list(map(lambda x:'test' if x=='' else x,a))
- Java IO<5>管道流PipedOutputStream PipedInputStream
在java中,PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流.它们的作用是让多线程可以通过管道进行线程间的通讯.在使用管道通信时,必须将PipedOu ...
- 二、第一个微信小程序
使用微信开发者工具创建一个新的空项目,即是一个显示自己账号的小程序. 也可以删除自动生成的冗余代码,手动写一个显示自己账号的简单小程序. 下面即是基于JavaScript模板的手工写的一个简单小程序. ...
- Java编码小技巧
你在写一个方法的时候, 例如传入 两个数组,而你要写的方法代码块又恰好有一种判断方式会导致你要写两个相同代码块, 你就可以自己调用自己,并把传参顺序 换一下 public int[] intersec ...