新闻详情

新闻详情

首页 / 资讯中心 / 详情

Elixir 宏编程实战:从 unless 重写到卫生机制、环境与 DSL 设计

发布时间:2026/10/1 9:39:24来源:尧图网络
Elixir 宏编程实战:从 unless 重写到卫生机制、环境与 DSL 设计
编程语言编译器标准库语言运行时并发编程【免费下载链接】elixirSimple from zero to scale项目地址https://gitcode.com/GitHub_Trending/el/elixir点击查看免费下载导读本文以 Elixir 官方指南 meta-programming/macros.md 为骨架系统讲解 Elixir 宏Macro的完整知识体系宏与普通函数的本质区别、defmacro/2的定义与展开机制、宏卫生hygiene与var!/1的上下文控制、__ENV__/0编译环境的读取、私有宏defmacrop以及负责任地编写宏的工程实践。读者学完本文后将掌握用quote/unquote接收并转换 AST 的核心能力并能安全地设计自己的 DSL。文中的源码证据均来自当前仓库可在 lib/elixir/lib/kernel.ex 与 lib/elixir/lib/macro.ex 中继续深入阅读。宏最后一招而不是第一选择Elixir 尽可能为宏提供一个安全的环境但编写干净宏代码的责任最终落在开发者身上。宏比普通 Elixir 函数更难写因此在不必要的时候使用宏被视为坏风格——请负责任地编写宏。Elixir 已经通过数据结构和函数提供了编写日常代码的简单、可读的机制宏只应在万不得已时使用。请记住两条原则显式优于隐式explicit is better than implicit清晰的代码优于简洁的代码clear code is better than concise code我们的第一个宏宏在 Elixir 中通过defmacro/2定义。本指南建议用文件而不是在 IEx 中逐行输入代码来演示——因为示例往往跨越多行全部在 IEx 里敲会很低效。你可以把示例保存到macros.exs文件中然后用elixir macros.exs或iex macros.exs运行。用函数和宏分别实现 unless为了理解宏的工作方式我们创建一个新模块分别以函数和宏两种方式实现unlessif/2的反向操作defmodule Unless do def fun_unless(clause, do: expression) do if(!clause, do: expression) end defmacro macro_unless(clause, do: expression) do quote do if(!unquote(clause), do: unquote(expression)) end end end函数版本接收参数并把它们传给if/2。而正如我们在 quote-and-unquote.md 中学习的宏接收的是引号化quoted的表达式把它们注入到quote中最终返回另一个 quoted 表达式。函数与宏的关键差异用iex macros.exs启动带上述模块的 IEx 并实验$ iex macros.exsiex require Unless iex Unless.macro_unless(true, do: IO.puts(this should never be printed)) nil iex Unless.fun_unless(true, do: IO.puts(this should never be printed)) this should never be printed nil在宏实现中句子没有被打印而在函数实现中它被打印了。原因是函数调用的参数在调用函数之前就被求值而宏不求值它们的参数——宏接收参数作为 quoted 表达式然后把它们转换成其他 quoted 表达式。在此例中unless宏在幕后被改写成if/2。换句话说当我们调用Unless.macro_unless(true, do: IO.puts(this should never be printed))macro_unless宏接收到的是macro_unless(true, [do: {{:., [], [{:__aliases__, [], [:IO]}, :puts]}, [], [this should never be printed]}])然后返回如下 quoted 表达式{:if, [], [{:!, [], [true]}, [do: {{:., [], [{:__aliases__, [], [:IO]}, :puts]}, [], [this should never be printed]}]]}用 Macro.expand_once/2 验证展开我们可以用Macro.expand_once/2实际验证这一点iex expr quote do: Unless.macro_unless(true, do: IO.puts(this should never be printed)) iex res Macro.expand_once(expr, __ENV__) iex IO.puts(Macro.to_string(res)) if !true do IO.puts(this should never be printed) end :okMacro.expand_once/2接收一个 quoted 表达式并根据当前环境展开它。在这个例子中它展开/调用了Unless.macro_unless/2宏并返回其结果。随后我们用Macro.to_string/1把返回的 quoted 表达式转成字符串打印出来__ENV__将在本章后面讨论。这就是宏的全部要义接收 quoted 表达式并把它们转换成别的东西。if/2 本身就是一个宏事实上Elixir 中的if/2就是作为宏实现的。在当前仓库的 lib/elixir/lib/kernel.ex 中可以看到它的真实实现defmacro if(condition, clauses) do build_if(condition, clauses) end defp build_if(condition, do: do_clause) do build_if(condition, do: do_clause, else: nil) end defp build_if(condition, do: do_clause, else: else_clause) do annotate_case( [optimize_boolean: true, type_check: {:case, :if}], quote do case unquote(condition) do x when unquote(x_is_false_or_nil()) - unquote(else_clause) _ - unquote(do_clause) end end ) end可以看到if/2在底层展开为一个带守卫的case表达式当条件为false或nil时返回else子句缺省为nil否则返回do子句。if/2、defmacro/2、def/2、defprotocol/2以及 Elixir 标准库中使用的许多其他构造都是纯 Elixir 编写的而且常常是宏。这意味着用于构建语言的构造开发者同样可以用来把语言扩展到他们工作的领域。不能覆盖的特殊形式Special Forms我们可以定义任何函数和宏包括覆盖 Elixir 内置定义的宏。唯一的例外是 Elixir 的特殊形式special forms——它们不是用 Elixir 实现的因此无法被覆盖。完整列表见Kernel.SpecialForms。例如在 lib/elixir/lib/kernel/special_forms.ex 中quote/2、require/2、import/2、__ENV__/0、__MODULE__/0、fn、、%{}、.运算符等都是特殊形式。宏卫生Macro HygieneElixir 宏具有**晚解析late resolution**特性保证 quote 内部定义的变量不会与宏展开处的上下文变量冲突。例如defmodule Hygiene do defmacro no_interference do quote do: a 1 end end defmodule HygieneTest do def go do require Hygiene a 13 Hygiene.no_interference() a end end HygieneTest.go() # 13上面的例子中即使宏注入了a 1它也不会影响go/0函数中定义的变量a。用 var!/1 显式影响上下文如果宏想显式影响调用方上下文可以使用var!/1defmodule Hygiene do defmacro interference do quote do: var!(a) 1 end end defmodule HygieneTest do def go do require Hygiene a 13 Hygiene.interference() a end end HygieneTest.go() # 1上面的代码可以工作但会发出警告variable a is unused。因为宏覆盖了原始值而原始值从未被使用。var!/2在内核中的实现位于 lib/elixir/lib/kernel.ex它会移除变量的:counter元数据并强制把变量当作普通上下文变量处理。变量上下文的表示变量卫生之所以有效是因为 Elixir 用**上下文context**标注变量。例如模块第 3 行定义的变量x会被表示为{:x, [line: 3], nil}而一个被 quote 的变量则表示为defmodule Sample do def quoted do quote do: x end end Sample.quoted() # {:x, [line: 3], Sample}注意 quoted 变量的第三个元素是原子Sample而不是nil这标志着变量来自Sample模块。因此 Elixir 认为这两个变量来自不同上下文并相应地处理它们。类似的机制也作用于 imports 和 aliases保证宏按照其源模块的行为执行而不是与宏被展开的目标模块冲突。卫生机制可以通过var!/2和alias!/1见 lib/elixir/lib/kernel.ex在特定情况下绕过但使用时必须小心因为它们会直接改变用户环境。动态生成变量Macro.var/2 与 Macro.unique_var/2有时变量名是动态创建的。这种情况下可以用Macro.var/2定义新变量defmodule Sample do defmacro initialize_to_char_count(variables) do Enum.map(variables, fn name - var Macro.var(name, nil) length name | Atom.to_string() | String.length() quote do unquote(var) unquote(length) end end) end def run do initialize_to_char_count([:red, :green, :yellow]) [red, green, yellow] end end Sample.run() # [3, 5, 6]注意Macro.var/2的第二个参数是上下文context它决定了下一节所述的卫生行为。其实现位于 lib/elixir/lib/macro.exdef var(var, context) when is_atom(var) and is_atom(context) do {var, [], context} end当你需要生成唯一名字的变量时还可以使用Macro.unique_var/2lib/elixir/lib/macro.ex每次调用都会带上自增计数器:counter从而保证唯一性。环境The Environment本章前面调用Macro.expand_once/2时我们使用了特殊形式__ENV__/0。__ENV__/0返回一个Macro.Env结构体定义于 lib/elixir/lib/macro/env.ex包含关于编译环境的有用信息包括当前模块、文件、行号、当前作用域中定义的所有变量以及 imports、requires 等iex __ENV__.module nil iex __ENV__.file iex iex __ENV__.requires [Application, IEx.Helpers, Kernel] iex require Integer Integer iex __ENV__.requires [Application, IEx.Helpers, Integer, Kernel]Macro模块中的许多函数都期望一个Macro.Env环境参数。你可以在 lib/elixir/lib/macro.ex 中阅读这些函数的更多细节并在Macro.Env中了解编译环境。Macro 模块AST 的操作工具箱Macro模块是操作 AST 的核心工具箱。其模块文档lib/elixir/lib/macro.ex开宗明义宏是编译期构造接收 Elixir 的 AST 作为输入返回 Elixir 的 AST 作为输出。它定义了t:input/0宏的输入AST、原子、数字、二进制等与t:output/0宏的输出还可以包含被捕获的远程函数与 pid。模块中常用的函数包括Macro.expand_once/2、Macro.expand/2按当前环境展开宏Macro.to_string/1把 AST 转回源码字符串见 lib/elixir/lib/macro.exMacro.escape/1把普通值转成可注入 quote 的 ASTMacro.var/2、Macro.unique_var/2构造变量节点Macro.traverse/4、Macro.postwalk/3、Macro.prewalk/3深度优先遍历与改写 AST。需要注意的是Macro模块中的函数不执行代码。事实上在宏中执行代码往往是反模式需要执行代码时请使用Code模块。私有宏Private MacrosElixir 还支持通过defmacrop定义私有宏。和私有函数一样这些宏只在定义它们的模块内可用且仅在编译期可用。defmacrop的定义同样在 lib/elixir/lib/kernel.ex。宏必须在使用之前定义。如果在调用之前没有定义宏运行时将抛出错误——因为宏不会被展开而是被翻译成一个函数调用iex defmodule Sample do ... def four, do: two() two() ... defmacrop two, do: 2 ... end ** (CompileError) iex:2: function two/0 undefined负责任地编写宏宏是强大的构造Elixir 提供了许多机制来确保它们被负责任地使用宏是卫生的hygienic默认情况下宏内部定义的变量不会影响用户代码。此外宏上下文中可用的函数调用和别名不会泄漏到用户上下文中。宏是词法的lexical不可能全局注入代码或宏。要使用宏你需要显式地require或import定义该宏的模块。宏是显式的explicit不显式调用宏就不可能运行它。例如有些语言允许开发者通过解析转换parse transforms或某些反射机制在后台完全重写函数而在 Elixir 中宏必须在编译期被调用方显式调用。宏的语言是清晰的许多语言为quote和unquote提供语法捷径Elixir 则选择把它们显式拼写出来以清晰地界定宏定义及其 quoted 表达式的边界。即使有这些保证编写负责任的宏时开发者也扮演重要角色。如果你确信需要诉诸宏请记住宏不是你的 API。保持宏简短把逻辑移到函数里保持宏定义包括其 quoted 内容短小精悍。例如与其这样写defmodule MyModule do defmacro my_macro(a, b, c) do quote do do_this(unquote(a)) # ... do_that(unquote(b)) # ... and_that(unquote(c)) end end end不如这样写defmodule MyModule do defmacro my_macro(a, b, c) do quote do # 这里只保留最小必要逻辑 # 其余全部移到函数中 MyModule.do_this_that_and_that(unquote(a), unquote(b), unquote(c)) end end def do_this_that_and_that(a, b, c) do do_this(a) ... do_that(b) ... and_that(c) end end这让代码更清晰、更容易测试和维护——你可以直接调用并测试do_this_that_and_that/3。它还能帮助你不依赖宏的开发者设计一个真正的 API。延伸从宏到 DSL至此本文完成了对宏的入门介绍。下一篇指南 domain-specific-languages.md 将简要讨论DSL如何混合使用宏与模块属性module attributes来注解和扩展模块与函数。回顾本文的完整知识链quote与unquote见 quote-and-unquote.md负责构造与注入 ASTdefmacro/2定义宏卫生机制保护调用方环境__ENV__/0提供编译环境上下文defmacrop提供模块内私有展开能力——这套机制正是 Elixir 元编程的基石也是标准库中if/2、def/2、defprotocol/2等语言级构造能够自举实现的原因所在。赞分享编程语言编译器标准库语言运行时并发编程【免费下载链接】elixirSimple from zero to scale项目地址https://gitcode.com/GitHub_Trending/el/elixir点击查看免费下载相关推荐cann-recipes-train性能调优秘籍MindSpeed框架与昇腾NPU协同优化cann recipes train性能调优秘籍MindSpeed框架与昇腾NPU协同优化 CANN / cann recipes train项目是针对LLM示例工程人工智能大模型预训练微调强化学习AscendDeepLearning4J op-codegen 可选参数与签名机制从 defaultValue 到 Signature 的 DSL 设计实践DeepLearning4J op codegen 可选参数与签名机制从 defaultValue 到 Signature 的 DSL 设计实践 导读 本文围深度学习人工智能机器学习分布式训练RustTraining 宏编程实战从 macro_rules! 到 syn quote 的过程宏RustTraining 宏编程实战从 macro_rules! 到 syn quote 的过程宏 本文是 RustTraining 开源训练仓库中 ru文档教程上一篇提升Vim操作效率如何通过vim-airline实时监控命令执行状态下一篇Redux Thunk与React Router路由与状态的协同创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

【人工智能】一定要去建立个人知识库 2026/10/1 10:20:10

【人工智能】一定要去建立个人知识库

【人工智能】一定要去建立个人知识库 一定要去建立个人知识库【人工智能】一定要去建立个人知识库1. 为什么普通人要建立个人知识库2. 使用个人知识库的四个步骤3. 让 AI 教你使用知识库4. 一个完整的闭环案例5. 建立个人知识库的六个步骤6. 结语摘要: 本文面向普通…

阅读更多 →
MAS激活工具使用指南:免费激活Windows与Office的4种方法与排障步骤 2026/10/1 10:19:57

MAS激活工具使用指南:免费激活Windows与Office的4种方法与排障步骤

MAS激活工具使用指南:免费激活Windows与Office的4种方法与排障步骤 【免费下载链接】Microsoft-Activation-Scripts Open-source Windows and Office activator featuring HWID, Ohook, TSforge, and Online KMS activation methods, along with advanced troublesh…

阅读更多 →
从“叙事溢价“到“临床数据定价“:157亿元融资潮下AI制药的估值错位与投资框架 2026/10/1 10:19:57

从“叙事溢价“到“临床数据定价“:157亿元融资潮下AI制药的估值错位与投资框架

【摘要】2026年至今AI制药赛道完成57笔融资、吸金157.65亿元,超过2023至2025年总和;全球173个AI发现药物在研,15个进入III期。按政策、管线、资本3线拆解一级与二级市场2套定价框架的错位成因,引入干湿闭环数据飞轮与实物期权定价…

阅读更多 →
远程管控iPhone,Realme家长用定位功能守护孩子出行 2026/10/1 10:19:57

远程管控iPhone,Realme家长用定位功能守护孩子出行

许多家长为了管控孩子的手机使用,每天下班回家后还要花费大量时间“排查”孩子的手机记录,在反复的叮嘱与监督中耗尽心力,甚至因此引发亲子间的争吵与隔阂,让家庭教育陷入两难境地。其实,亲子手机管控无需如此步履维艰…

阅读更多 →
全国制造业APP开发有哪些靠谱的开发公司? 2026/10/1 10:19:51

全国制造业APP开发有哪些靠谱的开发公司?

摘要:全国制造业APP开发公司是否靠谱,看它懂不懂车间现场:移动报工、扫码过站、生产看板、工单流转,而不是只做个领导看的报表。制造业App要对接MES、ERP、扫码设备,把车间数据实时采上来。选型看生产流程理解、硬件对…

阅读更多 →
操作系统个人学习笔记(四):K8s基础 2026/10/1 10:19:51

操作系统个人学习笔记(四):K8s基础

Pod Pod 是 K8s 的最小调度单位,里面可以有一个或多个容器,这些容器共享网络、存储等资源。 Pod 里的容器共享什么资源是否共享说明网络 namespace共享同一个 Pod 内容器共用 IP、端口空间,可以用 localhost 通信UTS namespace共享主机名相同…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉