(译) 理解 Elixir 中的宏 Macro, 第四部分:深入化
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ć.
在前一篇文章中, 我向你展示了分析输入 AST 并对其进行处理的一些基本方法. 今天我们将研究一些更复杂的 AST 转换. 这将重提已经解释过的技术. 这样做的目的是为了表明深入研究 AST 并不是很难的, 尽管最终的结果代码很容易变得相当复杂, 而且有点黑科技(hacky).
追踪函数调用
在本文中, 我们将创建一个宏 deftraceable
, 它允许我们定义可跟踪的函数. 可跟踪函数的工作方式与普通函数一样, 但每当我们调用它时, 都会打印出调试信息. 大致思路是这样的:
defmodule Test do
import Tracer
deftraceable my_fun(a,b) do
a/b
end
end
Test.my_fun(6,2)
# => test.ex(line 4) Test.my_fun(6,2) = 3
这个例子当然是虚构的. 你不需要设计这样的宏, 因为 Erlang 已经有非常强大的跟踪功能, 而且有一个 Elixir 包可用. 然而, 这个例子很有趣, 因为它需要一些更深层次的 AST 转换技巧.
在开始之前, 我要再提一次, 你应该仔细考虑你是否真的需要这样的结构. 例如 deftraceable
这样的宏引入了一个每个代码维护者都需要了解的东西. 看着代码, 它背后发生的事不是显而易见的. 如果每个人都设计这样的结构, 每个 Elixir 项目都会很快地变成自定义语言的大锅汤. 当代码主要依赖于复杂的宏时, 即使对于有经验的开发人员, 即使是有经验的开发人员也很难理解严重依赖于复杂宏的底层代码的实际流程.
但是在适当使用宏的情况下, 你不应该仅仅因为有人声称宏是不好的, 就不使用它. 例如, 如果在 Erlang 中没有跟踪功能, 我们就需要设计一些宏来帮助我们(实际上不需要类似上述的例子, 但那是另外一个话题), 否则我们的代码就会有大量重复的模板代码.
在我看来, 模板代码太多是不好的, 因为代码中有了太多形式化的噪音, 因此更难阅读和理解. 宏有助于减少这些噪声, 但在使用宏之前, 请先考虑是否可以优先使用 Elixir 内置的运行时结构(函数, 模块, 协议)来解决重复代码.
看完这个长长的免责声明, 让我们开始实现 deftraceable
吧. 首先, 手动生成对应的代码.
让我们回顾下用法:
deftraceable my_fun(a,b) do
a/b
end
生成的代码类似于这样:
def my_fun(a, b) do
file = __ENV__.file
line = __ENV__.line
module = __ENV__.module
function_name = "my_fun"
passed_args = [a,b] |> Enum.map(&inspect/1) |> Enum.join(",")
result = a/b
loc = "#{file}(line #{line})"
call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
IO.puts "#{loc} #{call}"
result
end
这个想法很简单. 我们从编译器环境中获取各种数据, 然后计算结果, 最后将所有内容打印到屏幕上.
该代码依赖于 __ENV__
特殊形式, 可用于在最终 AST 中注入各种编译时信息(例如行号和文件). __ENV__
是一个结构体, 每当你在代码中使用它时, 它将在编译时展开为适当的值. 因此, 只要在代码中写入 __ENV__.file
. 文件生成的字节码将包含包含文件名的(二进制)字符串常量.
现在我们需要动态构建这个代码. 让我们来看看大概的样子(outline):
defmacro deftraceable(??) do
quote do
def unquote(head) do
file = __ENV__.file
line = __ENV__.line
module = __ENV__.module
function_name = ??
passed_args = ?? |> Enum.map(&inspect/1) |> Enum.join(",")
result = ??
loc = "#{file}(line #{line})"
call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
IO.puts "#{loc} #{call}"
result
end
end
end
这里我们在需要基于输入参数动态注入 AST 片段的地方放置问号(??). 特别地, 我们必须从传递的参数中推导出函数名、参数名和函数体.
现在, 当我们调用宏 deftraceable my_fun(...) do ... end
, 宏接收两个参数 — 函数头(函数名和参数列表)和包含函数体的关键字列表. 这些都是被 quote 过的.
我是如何知道的?其实我不知道. 我一般通过不断试错来获得的这些信息. 基本上, 我从定义一个宏开始:
defmacro deftraceable(arg1) do
IO.inspect arg1
nil
end
然后我尝试从一些测试模块或 shell 中调用宏. 我将通过向宏定义中添加另一个参数来测试. 一旦我得到结果, 我会试图找出参数表示什么, 然后开始构建宏.
宏结束处的 nil
确保我们不生成任何东西(我们生成的 nil
通常与调用者代码无关). 这允许我进一步构建片段而不注入代码. 我通常依靠 IO.inspect
和 Macro.to_string/1
来验证中间结果, 一旦我满意了, 我会删除 nil
部分, 看看是否能工作.
此时 deftraceable
接收函数头和身体. 函数头将是一个我们之前描述的结构的 AST 片段:
{function_name, context, [arg1, arg2, ...]
所以接下来我们需要:
- 从 quoted 的头中提取函数名和参数
- 将这些值注入我们的宏返回的 AST 中
- 将函数体注入同一个 AST
- 打印跟踪信息
我们可以使用模式匹配从这个 AST 片段中提取函数名和参数, 有一个 Macro.decompose_call/1
的辅助功能函数可以帮我们做到. 做完这些步骤, 宏的最终版本实现如下所示:
defmodule Tracer do
defmacro deftraceable(head, body) do
# 提取函数名和参数
{fun_name, args_ast} = Macro.decompose_call(head)
quote do
def unquote(head) do
file = __ENV__.file
line = __ENV__.line
module = __ENV__.module
# 注入函数名和参数到 AST 中
function_name = unquote(fun_name)
passed_args = unquote(args_ast) |> Enum.map(&inspect/1) |> Enum.join(",")
# 将函数体注入到 AST
result = unquote(body[:do])
# 打印 trace 跟踪信息
loc = "#{file}(line #{line})"
call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"
IO.puts "#{loc} #{call}"
result
end
end
end
end
让我们试一下:
iex(1)> defmodule Tracer do ... end
iex(2)> defmodule Test do
import Tracer
deftraceable my_fun(a,b) do
a/b
end
end
iex(3)> Test.my_fun(10,5)
iex(line 4) Test.my_fun(10,5) = 2.0 # trace output
2.0
这似乎起作用了. 然而, 我应该立即指出, 这种实现存在一些问题:
- 宏不能很好地处理带守卫(guards)的函数定义
- 模式匹配参数并不总是有效的(例如, 当使用 _ 来匹配任何 term 时)
- 在模块中直接动态生成代码时, 宏不起作用.
我将逐一解释这些问题, 首先从守卫(guards)开始, 其余问题留待以后的文章再讨论.
处理 guards (守卫)
所有具有可追溯性的问题都源于我们对输入 AST 做了一些事实假设. 这是一个危险的领域, 我们必须小心地涵盖所有情况.
例如, 宏假设 head 只包含函数名称和参数列表. 因此, 如果我们想定义一个带守卫的可跟踪函数, deftraceable
将不起作用:
deftraceable my_fun(a,b) when a < b do
a/b
end
在这种情况下, 我们的头部(宏的第一个参数)也将包含守卫(guards)的信息, 并且不能被 macro .decompose_call/1
解析. 解决方案是检测这种情况, 并以一种特殊的方式处理它.
首先, 让我们来看看这个 head 是如何被 quoted 的:
iex(16)> quote do my_fun(a,b) when a < b end
{:when, [],
[
{:my_fun, [],
[{:a, [if_undefined: :apply], Elixir}, {:b, [if_undefined: :apply], Elixir}]},
{:<, [context: Elixir, import: Kernel],
[{:a, [if_undefined: :apply], Elixir}, {:b, [if_undefined: :apply], Elixir}]}
]}
所以实际上我们的 guard head 实际上是这样的: {:when, _, [name_and_args, ...]}
, 我们可以依靠它来使用模式匹配提取函数名称和参数:
defmodule Tracer do
...
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
...
当然, 我们需要从宏中调用这个函数:
defmodule Tracer do
...
defmacro deftraceable(head, body) do
{fun_name, args_ast} = name_and_args(head)
... # 不变
end
...
end
如您所见, 可以定义额外的私有函数并从宏调用它们. 毕竟, 宏只是一个函数, 当调用它时, 包含的模块已经编译并加载到编译器的 VM 中(否则, 宏无法运行).
以下是宏 deftraceable
的完整版本:
defmodule Tracer do
defmacro deftraceable(head, body) do
{fun_name, args_ast} = name_and_args(head)
quote do
def unquote(head) do
file = __ENV__.file
line = __ENV__.line
module = __ENV__.module
function_name = unquote(fun_name)
passed_args = unquote(args_ast) |> 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
end
让我们来试验一下:
iex(1)> defmodule Tracer do ... end
iex(2)> defmodule Test do
import Tracer
deftraceable my_fun(a,b) when a<b do
a/b
end
deftraceable my_fun(a,b) do
a/b
end
end
iex(3)> Test.my_fun(5,10)
iex(line 4) Test.my_fun(5,10) = 0.5
0.5
iex(4)> Test.my_fun(10, 5)
iex(line 7) Test.my_fun(10,5) = 2.0
这个练习的主要目的是说明可以从输入 AST 中推断出一些东西. 在这个例子中, 我们设法检测和处理带 guards 的函数. 显然, 因为它依赖于 AST 的内部结构, 代码变得更加复杂了. 在这种情况下, 代码依旧比较简单, 但你将在后面的文章 《(译) Understanding Elixir Macros, Part 5 - Reshaping the AST》 中看到我是如何解决 deftraceable
宏剩余的问题的, 事情可能很快变得复杂起来了.
本文由博客群发一文多发等运营工具平台 OpenWrite 发布
(译) 理解 Elixir 中的宏 Macro, 第四部分:深入化的更多相关文章
- [NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()
译者注: 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正. 文末会有几个小问题,大家不妨一起思考一下 ...
- C/C++ 中的宏/Macro
宏(Macro)本质上就是代码片段,通过别名来使用.在编译前的预处理中,宏会被替换为真实所指代的代码片段,即下图中 Preprocessor 处理的部分. C/C++ 代码编译过程 - 图片来自 nt ...
- uboot中的中断macro宏
目录 uboot中的中断macro宏 引入 内存分配 流程概览 普通中断 保存现场 中断函数打印具体寄存器 恢复现场 软中断 空间获取 保存现场 附录速记 疑惑待解 title: uboot中的中断m ...
- [译]线程生命周期-理解Java中的线程状态
线程生命周期-理解Java中的线程状态 在多线程编程环境下,理解线程生命周期和线程状态非常重要. 在上一篇教程中,我们已经学习了如何创建java线程:实现Runnable接口或者成为Thread的子类 ...
- 【译】理解JavaScript中的柯里化
译文开始 函数式编程是一种编程风格,这种编程风格就是试图将传递函数作为参数(即将作为回调函数)和返回一个函数,但没有函数副作用(函数副作用即会改变程序的状态). 有很多语言采用这种编程风格,其中包括J ...
- 【译】理解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 ...
随机推荐
- C++基础——引用和指针篇
一.指针(Pointer) 定义: 指针是一个变量,用于存储另一个变量的地址. 基本用法: #include <iostream> using namespace std; int mai ...
- 原生JS表格数据常用总结
主要是在数据报表这块, 做了好几年发现, 其实用户最终想要看的并不是酷炫的BI大屏, 而是最基础也是最复杂的 中国式报表. 更多就是倾向于从表格中去获取数据信息, 最简单的就是最好的, 于是还是来总结 ...
- IDEA开启热加载
然後 Ctrl+Shift+Ait+/ pom.xml里添加 <build> <plugins> <!-- 配置插件,让热部署依赖spring-boot-devtools ...
- WordPress插件:dsdiss-ai-paraphrasing(火山引擎版)文章伪原创 !一键创作!升级!
本站原创!<文章伪原创插件(火山引擎版)>是一款基于WordPress开发的高效内容创作工具,依托火山方舟AI模型实现智能化文章伪原创处理.插件支持在后台配置API密钥与模型ID,可灵活设 ...
- 端到端自动驾驶系统实战指南:从Comma.ai架构到PyTorch部署
引言:端到端自动驾驶的技术革命 在自动驾驶技术演进历程中,端到端(End-to-End)架构正引领新一轮技术革命.不同于传统分模块处理感知.规划.控制的方案,端到端系统通过深度神经网络直接建立传感器原 ...
- VMware Workstation 部署企业级 AD 域、DNS、DHCP 系统操作指南
一.主机环境规划 1. 虚拟机配置表 主机角色 操作系统 IP 地址 子网掩码 DNS 内存 硬盘 网络模式 域控制器 (DC) Windows Server 2022 192.168.1.10 25 ...
- Web前端入门第 59 问:JavaScript 条件语句中善用 return 让代码更清晰
条件语句 JS 的条件语句不太多,就 if 和 switch 两个,不过他们的使用方式也可以算是眼花缭乱了. if 语句 if 字面意思:如果 xxx.程序中的用法也是这样,如果条件为真,则执行执行代 ...
- odoo15接口调用qweb打印,将pdf旋转并下载到本地
一.将pdf旋转的通用方法 def rotate_pdf(self, pdf, angle): """ rotateClockwise(90) 这里的pdf传:bytes ...
- Spring Ai 从Demo到搭建套壳项目(一)初识与实现与deepseek对话模式
前言 为什么说Java长青,主要是因为其生态圈完善,Spring又做了一款脚手架,把对接各个LLM厂商的sdk做了一遍,形成一系列的spring-ai-starter-** 的依赖. 目前为止版本去到 ...
- Spring扩展接口-BeanFactoryPostProcessor
.markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...