(译) 理解 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 doimport Tracerdeftraceable my_fun(a,b) doa/bend
endTest.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) doa/b
end

生成的代码类似于这样:

def my_fun(a, b) dofile = __ENV__.fileline = __ENV__.linemodule = __ENV__.modulefunction_name = "my_fun"passed_args = [a,b] |> Enum.map(&inspect/1) |> Enum.join(",")result = a/bloc = "#{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(??) doquote dodef unquote(head) dofile = __ENV__.fileline = __ENV__.linemodule = __ENV__.modulefunction_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}"resultendend
end

这里我们在需要基于输入参数动态注入 AST 片段的地方放置问号(??). 特别地, 我们必须从传递的参数中推导出函数名、参数名和函数体.

现在, 当我们调用宏 deftraceable my_fun(...) do ... end, 宏接收两个参数 — 函数头(函数名和参数列表)和包含函数体的关键字列表. 这些都是被 quote 过的.

我是如何知道的?其实我不知道. 我一般通过不断试错来获得的这些信息. 基本上, 我从定义一个宏开始:

defmacro deftraceable(arg1) doIO.inspect arg1nil
end

然后我尝试从一些测试模块或 shell 中调用宏. 我将通过向宏定义中添加另一个参数来测试. 一旦我得到结果, 我会试图找出参数表示什么, 然后开始构建宏.

宏结束处的 nil 确保我们不生成任何东西(我们生成的 nil 通常与调用者代码无关). 这允许我进一步构建片段而不注入代码. 我通常依靠 IO.inspectMacro.to_string/1 来验证中间结果, 一旦我满意了, 我会删除 nil 部分, 看看是否能工作.

此时 deftraceable 接收函数头和身体. 函数头将是一个我们之前描述的结构的 AST 片段:

{function_name, context, [arg1, arg2, ...]

所以接下来我们需要:

  • 从 quoted 的头中提取函数名和参数
  • 将这些值注入我们的宏返回的 AST 中
  • 将函数体注入同一个 AST
  • 打印跟踪信息

我们可以使用模式匹配从这个 AST 片段中提取函数名和参数, 有一个 Macro.decompose_call/1 的辅助功能函数可以帮我们做到. 做完这些步骤, 宏的最终版本实现如下所示:

defmodule Tracer dodefmacro deftraceable(head, body) do# 提取函数名和参数{fun_name, args_ast} = Macro.decompose_call(head)quote dodef unquote(head) dofile = __ENV__.fileline = __ENV__.linemodule = __ENV__.module# 注入函数名和参数到 AST 中function_name = unquote(fun_name)passed_args = unquote(args_ast) |> Enum.map(&inspect/1) |> Enum.join(",")# 将函数体注入到 ASTresult = unquote(body[:do])# 打印 trace 跟踪信息loc = "#{file}(line #{line})"call = "#{module}.#{function_name}(#{passed_args}) = #{inspect result}"IO.puts "#{loc} #{call}"resultendendend
end

让我们试一下:

iex(1)> defmodule Tracer do ... endiex(2)> defmodule Test doimport Tracerdeftraceable my_fun(a,b) doa/bendendiex(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 doa/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 | _]}) doname_and_args(short_head)enddefp name_and_args(short_head) doMacro.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 dodefmacro deftraceable(head, body) do{fun_name, args_ast} = name_and_args(head)quote dodef unquote(head) dofile = __ENV__.fileline = __ENV__.linemodule = __ENV__.modulefunction_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}"resultendendenddefp name_and_args({:when, _, [short_head | _]}) doname_and_args(short_head)enddefp name_and_args(short_head) doMacro.decompose_call(short_head)end
end

让我们来试验一下:

iex(1)> defmodule Tracer do ... endiex(2)> defmodule Test doimport Tracerdeftraceable my_fun(a,b) when a<b doa/benddeftraceable my_fun(a,b) doa/bendendiex(3)> Test.my_fun(5,10)
iex(line 4) Test.my_fun(5,10) = 0.5
0.5iex(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 宏剩余的问题的, 事情可能很快变得复杂起来了.

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

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

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

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

相关文章

如何开启MySQL的binlog日志

1.启用远程连接&#xff1a; 如果你想要允许远程主机连接到MySQL服务器&#xff0c;需要进行以下步骤&#xff1a; 确保MySQL服务器的防火墙允许远程连接的流量通过。在MySQL服务器上&#xff0c;编辑MySQL配置文件&#xff08;一般是my.cnf&#xff09;&#xff0c;找到bind-…

Go——函数

一. 函数定义 1.1 特点 无需声明原型支持不定变参支持多返回值支持命名返回参数支持匿名函数和闭包函数也是一种类型&#xff0c;一种函数可以赋值给变量不支持嵌套&#xff0c;一个包不能有两个名字一样的函数不支持重载不支持默认参数 1.2 函数声明 函数声明包含一个函数名&…

备战蓝桥杯---DP刷题2

1.树形DP&#xff1a; 即问那几个点在树的直径上&#xff0c;类似ROAD那题&#xff0c;我们先求一下每一个子树根的子树的最大值与次大值用d1,d2表示&#xff0c;直径就是d1d2的最大值&#xff0c;那么我们如何判断是否在最大路径上&#xff0c;其实就是看一下从某一点出发的所…

还得是抖音,字节推出竖屏视频理解数据集,入选CVPR2024

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 短视频在当下社交媒体逐渐成为主导的视频格式。传统视频处理技术和研究一般都专注于横屏视频…

58商铺全新UI试客试用平台网站php源码

探索未来商铺新纪元&#xff0c;58商铺全新UI试客试用平台网站PHP源码完整版震撼来袭&#xff01; 在这个数字化飞速发展的时代&#xff0c;58商铺一直致力于为商家和消费者打造更加便捷、高效的交易平台。今天&#xff0c;我们荣幸地推出全新UI试客试用平台网站PHP源码完整版…

计算模型 观察分析 杂记

计算模式 计算模式通常指的&#xff1a;用特定计算资源完成特定计算任务所采用的计算策略。计算资源主要指运算器和存储器&#xff0c;当然若其他设备影响因素较大的情况下也考虑控制器&#xff0c;输入输出设备&#xff1b;计算任务多种多样&#xff0c;可以是简单的加减乘除&…

动态规划基础

动态规划 1、动态规划的概念 简称DP,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。常常适用于有重叠子问题和最优子结构性质的问题。 简单来说,就是给定一个问题,把它拆成一个个子问题,查到子问题可以直接解决。然后把子问题答案保存起来,以减少重复计算…

Flink SQL系列之:解析Debezium数据格式时间字段常用的函数

Flink SQL系列之:解析Debezium数据格式时间字段常用的函数 一、FROM_UNIXTIME二、DATE_FORMAT三、TO_DATE四、CAST五、TO_TIMESTAMP_LTZ六、CONVERT_TZ七、FROM_UNIXTIME八、TO_TIMESTAMP九、常见用法案例1.案例一2.案例二3.案例三4.案例四5.案例五

C/C++ 项目:分别用精密星历和广播星历计算卫星坐标

文章目录 Part.I IntroductionChap.I rinex.hChap.II gmain_body.h Part.II 使用方法扩展阅读 Part.I Introduction 本文将介绍一个小项目的使用方法&#xff0c;此项目可用精密星历和广播星历计算卫星位置&#xff0c;并将两者结果做差&#xff0c;输出至文件。 其实 『分别…

SWM341系列应用(上位机应用)

SWM341系列之上位机应用 1、分级图像和PNG、JPG的应用 现象&#xff1a;客户使用SWM34SVET6HMI_0.4.1版本上位机进行UI界面布局&#xff0c;反馈在模拟运行时&#xff08;PC端&#xff09;流畅&#xff0c;在Demo平台&#xff08;设备端&#xff09;运行卡顿。 分析及解决&…

【fastadmin】脚本模式下,日志钩子函数执行出现死循环,导致内存溢出奔溃

问题出现原因是想对项目中error级别的日志&#xff0c;接入钉钉告警&#xff0c;方便查看 于是使用钩子方法&#xff0c;日志写入完成后&#xff0c;自动调用自定义的告警方法中 1、在application/tags.php 中添加log_write_done > [app\\common\\behavior\\Common, ],2、在…

【THM】Nmap Post Port Scans(后端口扫描)-初级渗透测试

介绍 本房间是 Nmap 系列的最后一个(网络安全简介模块的一部分)。在这个房间中,我们重点关注端口扫描之后的步骤:特别是服务检测、操作系统检测、Nmap脚本引擎和保存扫描结果。 Nmap实时主机发现Nmap基本端口扫描Nmap高级端口扫描Nmap后端口扫描在本系列的第一个房间中,我…

ZJGSU 1858在数组中查找两个数之和等于输入的另一个数

描述 题目&#xff1a;输入一个已经按升序排序过的数组和一个数字&#xff0c; 在数组中查找两个数&#xff0c;使得它们的和正好是输入的那个数字。如果有多对数字的和等于输入的数字&#xff0c;输出任意一对即可。 例如输入数组1、2、4、7、11、15和数字15。由于41115&…

代码随想录第29天|491.递增子序列 46.全排列 47.全排列 II

目录&#xff1a; 491.递增子序列 46.全排列 47.全排列 II 491.递增子序列 491. 非递减子序列 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 回溯算法精讲&#xff0c;树层去重与树枝去重 | LeetCode&#xff1a;491.递增子序列_哔哩哔哩_bili…

DTFT及其反变换的直观理解

对于离散时间傅里叶变换(DTFT)及其反变换的讲解&#xff0c;教材里通常会先给出DTFT正变换的公式&#xff0c;再举个DTFT的简单变换例子&#xff0c;推导一下DTFT的性质&#xff0c;然后给出DTFT反变换的公式&#xff0c;再证明一下正变换和反变化的对应关系。总的来说就是&…

Spring-IoC 基于xml管理

现大多使用注解方式&#xff0c;xml方式并不简洁&#xff0c;本文仅记录xml用作基础学习。 0、前提 首先在父项目的pom.xml中配置好依赖们。然后子模块也可以使用这些依赖。 在resource目录下创建Spring的xml文件&#xff0c;名称无要求&#xff0c;本文使用bean.xml。文件最…

黄锈水过滤器 卫生热水工业循环水色度水处理器厂家工作原理动画

​ 1&#xff1a;黄锈水处理器介绍 黄锈水处理器是一种专门用于处理“黄锈水”的设备&#xff0c;它采用机电一体化设计&#xff0c;安装方便&#xff0c;操作简单&#xff0c;且运行费用极低。这种处理器主要由数码射频发生器、射频换能器、活性过滤体三部分组成&#xff0c;…

uniapp uni.scss中使用@mixin混入,在文件引入@include 样式不生效 Error: Undefined mixin.(踩坑记录一)

问题&#xff1a; 在uni.scss文件定义mixin 2. 在vue文件引入: 3. 出现报错信息: 4. 问题思考&#xff1a; 是不是需要引入uni.scss &#xff1f; 答案不需要 uni.scss是一个特殊文件&#xff0c;在代码中无需 import 这个文件即可在scss代码中使用这里的样式变量。uni-app的…

图像识别技术在体育领域的应用

图像识别技术在体育领域的应用是一个充满创新和挑战的研究方向。随着计算机视觉和人工智能技术的快速发展&#xff0c;图像识别技术已经在体育领域展现出广泛的应用潜力和实际价值。以下是一些图像识别技术在体育领域的具体应用&#xff1a; 运动员表现分析&#xff1a; 图像识…

原创【matcap材质在ue4中的实现办法】

matcap材质在ue4中的实现办法 2023-08-29 15:34 https://www.bilibili.com/video/BV1GR4y1b76n/?spm_id_from333.337.search-card.all.click&vd_sourced76b773892c830a157c0ccc97ba78411 评论(0)