(译) 理解 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 ... endiex(2)> defmodule Test doimport Tracerdeftraceable div(_, 0), do: :errorend
** (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 onesquote dodef unquote(head) do... # 不变# 使用临时变量构造追踪信息passed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",")... # 不变endend
end

首先, 我们从函数头(head)提取函数名称和 args (我们在前一篇文章中解决了这个问题). 然后, 我们必须将 = argX 注入到 args_ast 中, 并收回修改后的参数(我们将将其放入 decorated_args中).

我们还需要生成的变量的纯名称(或者更确切地说是它们的 AST), 因为我们将使用这些名称来收集参数值. 变量 arg_names 实际上包含 quote do [arg_1, arg_2, ....] end, 可以很容易地注入到 AST 树中.

我们来实现剩下的部分. 首先, 让我们看看如何修饰参数:

defp decorate_args(args_ast) dofor {arg_ast, index} <- Enum.with_index(args_ast) do# 动态生成 quoted 标识符arg_name = Macro.var(:"arg#{index}", __MODULE__)# 为 patternX = argX 生成 ASTfull_arg = quote dounquote(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). 使用 unzipto_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. 递归地遍历 ASThead = Macro.postwalk(head,# lambda 函数处理输入 AST 中的元素, 返回修改过的 ASTfn# 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) -> otherend)... # 不变
end

Macro.postwalk/2 递归地遍历 AST, 并且在所有节点的后代被访问之后, 为每个节点调用提供的 lambda 函数. lambda 函数接收元素的 AST, 这样我们有机会返回一些除了指定节点之外的东西.

我们在这个 lambda 里做的实际上是一个模式匹配, 我们在寻找 {fun_name, context, args}. 如第三篇文章中所述那样, 这是表达式 some_fun(arg1, arg2, ...) 的 quoted 表现形式. 一旦我们遇到匹配此模式的节点, 我们只需要用新的(修饰过的)输入参数替换掉旧的. 在所有其它情况下, 我们简单地返回输入的 AST, 使得树的其余部分不变.

这看着有点复杂了, 但它解决了我们的问题. 以下是 deftraceable 宏的最终版本:

defmodule Tracer dodefmacro 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) -> otherend)quote dodef unquote(head) dofile = __ENV__.fileline = __ENV__.linemodule = __ENV__.modulefunction_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}"resultendendenddefp name_and_args({:when, _, [short_head | _]}) doname_and_args(short_head)enddefp name_and_args(short_head) doMacro.decompose_call(short_head)enddefp decorate_args([]), do: {[],[]}defp decorate_args(args_ast) dofor {arg_ast, index} <- Enum.with_index(args_ast) do# 动态生成 quoted 标识符(identifier)arg_name = Macro.var(:"arg#{index}", __MODULE__)# 为 patternX = argX 构建 ASTfull_arg = quote dounquote(arg_ast) = unquote(arg_name)end{arg_name, full_arg}end|> Enum.unzipend
end

让我们来试试:

iex(1)> defmodule Tracer do ... endiex(2)> defmodule Test doimport Tracerdeftraceable div(_, 0), do: :errordeftraceable div(a, b), do: a/bendiex(3)> Test.div(5, 2)
iex(line 6) Elixir.Test.div(5,2) = 2.5iex(4)> Test.div(5, 0)
iex(line 5) Elixir.Test.div(5,0) = :error

正如你所看到的那样, 可以进入 AST, 分解它, 并在其中散布一些自定义的注入代码, 这并不算很复杂. 缺点是, 编写的宏的代码会变得越来越复杂, 并且更难分析.

今天的话题到此结束. 下一次, 我将讨论原地代码生成技术 《(译) Understanding Elixir Macros, Part 6 - In-place Code Generation》.

原文: https://www.theerlangelist.com/article/macros_5

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/799637.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Labview】虚拟仪器技术

一、背景知识 1.1 虚拟仪器的定义、组成和应用 虚拟仪器的特点 虚拟仪器的突出特征为“硬件功能软件化”&#xff0c;虚拟仪器是在计算机上显示仪器面板&#xff0c;将硬件电路完成信号调理和处理功能由计算机程序完成。 虚拟仪器的组成 硬件软件 硬件是基础&#xff0c;负责将…

数字化转型回归底层逻辑

2024年第一季度已然过去&#xff0c;对于我们每个人而言&#xff0c;这都是一次严峻的挑战。2015年&#xff0c;蚓链数字化营销系统踏上研发之路。让我们回顾数字技术对企业的影响阶段&#xff1a;2012-2015 年&#xff0c;互联网在消费端产生巨大影响&#xff0c;引发了虚拟经…

清空nginx缓存并强制刷新

当对nginx的文件进行修改或更新时&#xff0c;可能会出现旧文件被缓存而无法立即生效的问题&#xff0c;此时需要清空nginx的文件缓存并强制刷新。可以通过以下步骤实现&#xff1a; 登录nginx服务器执行命令&#xff1a;sudo nginx -s reload &#xff08;重新加载nginx配置&…

YOLOv5改进--轻量化YOLOv5s模型

文章目录 1、前言2、轻量化模型结构&#xff1a;3、模型对比4、训练结果图5、目标检测文章 1、前言 在边缘设备的场景下&#xff0c;目前的YOLOv5s&#xff0c;虽然能够快速实现目标检测&#xff0c;但是运行速度依旧稍慢点&#xff0c;本文在牺牲一点精度前提下&#xff0c;提…

Web漏洞-文件上传常见验证

后缀名&#xff1a;类型&#xff0c;文件头等 后缀名&#xff1a;黑白名单 文件类型&#xff1a;MIME信息 文件头&#xff1a;内容头信息 常见黑名单&#xff08;明确不允许上传的格式后缀&#xff09;&#xff1a;asp、php、jsp、aspx、cgi、war &#xff08;如果没有完整…

vscode:插件开发

文档&#xff1a; 连接 终端执行 sudo pnpm install -g yo generator-codeyo code// 这里建议选择 JavaScript 很少出错 # ? What type of extension do you want to create? New Extension (JavaScript) # ? Whats the name of your extension? HelloWorld ### Press <…

nacos derby.log无法的读取+derby数据库启动失败分析解决

排查思路分析 日志报错&#xff1a; derby.log文件权限不够&#xff08;root权限&#xff09;&#xff0c;无法读取&#xff0c;我用普通用户启动。 使用命令chown xx:xx derby.log修改属主和属组为普通用户后&#xff0c;又报出其他错误。 数据库启动不了&#xff0c;无…

Composer Windows 安装

Composer 的下载地址为&#xff1a;Composer 1 运行安装程序 当启动安装程序后单击下一步继续。 选择 PHP 路径 如果你的计算机上没有安装 PHP 的话&#xff0c;Composer 的安装无法继续。 你需要选择你本地安装的 PHP 路径。 配置代理地址 默认的情况下&#xff0c;可以不…

【LeetCode刷题记录】15. 三数之和

15 三数之和 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [ n u m s [ i ] , n u m s [ j ] , n u m s [ k ] ] [nums[i], nums[j], nums[k]] [nums[i],nums[j],nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 n u m s [ i ] n u m s [ j ] n u …

java基础语法(11)| 内部类

1. 内部类 1.1 什么是内部类 将一个类A定义在另一个类B里面&#xff0c;里面的那个类A就称为内部类&#xff0c;B则称为外部类。 1.2 内部类的分类 成员内部类 局部内部类 匿名内部类 1.3 成员内部类 在描述事物时&#xff0c;若一个事物内部还包含其他事物&#xff0c;就…

基于starganvc2的变声器论文原理解读

数据与代码见文末 论文地址&#xff1a;https://arxiv.org/pdf/1907.12279.pdf 1.概述 什么是变声器&#xff0c;变声器就是将语音特征进行转换&#xff0c;而语音内容不改变 那么我们如何构建一个变声器呢&#xff1f; 首先&#xff0c;我们肯定不能为转换的每一种风格的声…

JavaEE初阶——多线程(一)

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 小比特 大梦想 此篇文章与大家分享多线程的第一部分:引入线程以及创建多线程的几种方式 此文章是建立在前一篇文章进程的基础上的 如果有不足的或者错误的请您指出! 1.认识线程 我们知道现代的cpu大多都是多核心…

【Figma】安装指南及基础操作

先前做UI设计一直都是用PS等绘图软件设计&#xff0c;但发现在纠结像素和排版问题上会花很多时间&#xff0c;再加上AI没来得及上手&#xff0c;就需要迅速出成图&#xff0c;此时通过论坛发现了figma&#xff0c;基本上可以满足足够的需求&#xff0c;并且可以在windows系统上…

【云开发笔记NO.27】分布式数据库

在云原生开发中&#xff0c;分布式数据库是一种特殊类型的数据库管理系统&#xff0c;其数据不是存储在单个集中的服务器上&#xff0c;而是分散在网络中的多个节点或服务器上。这种分散存储的方式使得分布式数据库具有很高的可扩展性、容错性和性能优势。云原生环境下的分布式…

SVG图标显示

SVG图标显示 1.安装SharpVectors.Wpf包 2.添加引用 xmlns:svgc"http://sharpvectors.codeplex.com/svgc/"3.加载svg文件&#xff0c;生成操作选择资源(Resource) 4.UI界面显示SVG图像 <Button Click"OnSaveFileClick" ToolTip"Save Svg File…

武汉星起航:跨境电商领域崭露头角,以实战经验引领行业新潮流

随着全球经济的演进&#xff0c;跨境电商正成为国际贸易的引擎&#xff0c;而武汉星起航电子商务有限公司凭借其丰富实战经验和专业团队&#xff0c;在这一领域取得了引人瞩目的成绩。公司成立于2020年&#xff0c;自2017年起紧紧围绕亚马逊自营店铺&#xff0c;致力于为合作伙…

基于Spring Boot+Vue的在线拍卖系统

随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统&#xff0c;主要的模块包括管理员&#xff1b;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单管理、…

初学python记录:力扣1600. 王位继承顺序

题目&#xff1a; 一个王国里住着国王、他的孩子们、他的孙子们等等。每一个时间点&#xff0c;这个家庭里有人出生也有人死亡。 这个王国有一个明确规定的王位继承顺序&#xff0c;第一继承人总是国王自己。我们定义递归函数 Successor(x, curOrder) &#xff0c;给定一个人…

通用接口开放平台设计与实现——(30)消息服务端之消息重发优化

背景 消息服务通信机制为异步&#xff0c;且网络连接不是100%可靠&#xff0c;会因为网络闪断问题丢失消息&#xff0c;作为企业应用&#xff0c;需要保证业务消息传输的可靠性&#xff0c;需实现以下机制&#xff1a; a)发送方重发机制&#xff1a;消息发送方对未收到响应的消…

谷歌(Google)技术面试——全部面试流程

简介 本篇介绍一下谷歌的全部面试流程,包括两次电话面试和五次现场面试。之后便是招聘委员会评定和录用审核。 Google 电话面试 电话面试通常会持续 30 至 60 分钟。 电话面试的内容涵盖数据结构与算法。准备好用你最擅长的编程语言编写 20 至 30 行代码。代码应该保持整洁…