上一章我们提出了一个基本版的deftraceable宏,能让我们编写可跟踪的函数。宏的最终版本有一些剩余的问题,今天我们将解决其中的一个——参数模式匹配。

今天的练习表明我们必须仔细考虑宏可能接收到的输入。

问题

正如我上一次暗示的那样,当前版本的deftraceable不适用于模式匹配的参数。让我们来演示一下这个问题:

iex(1)> defmodule Tracer do ... endiex(2)> defmodule Test doimport Tracerdeftraceable div(_, 0), do: :errorend
** (CompileError) iex:5: unbound variable _

发生了什么?deftraceable宏盲目地将输入的参数当做是纯变量或常量。因此,当你调用deftraceable 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)# Decorates input args by adding "= argX" to each argument.# Also returns a list of argument names (arg1, arg2, ...){arg_names, decorated_args} = decorate_args(args_ast)head = ??   # Replace original args with decorated onesquote dodef unquote(head) do... # unchanged# Use temp variables to make a trace messagepassed_args = unquote(arg_names) |> Enum.map(&inspect/1) |> Enum.join(",")... # unchangedendend
end

首先,我们从头中提取名称和参数(我们在前一篇文章中已经解决了)。然后,我们必须在args_ast中注入= argX,并取回修改过的args(我们会将其放入decorated_args)。

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

现在让我们实现其余的。首先,让我们看看如何装饰参数:

defp decorate_args(args_ast) dofor {arg_ast, index} <- Enum.with_index(args_ast) do# Dynamically generate quoted identifierarg_name = Macro.var(:"arg#{index}", __MODULE__)# Generate AST for patternX = argXfull_arg = quote dounquote(arg_ast) = unquote(arg_name)end{arg_name, full_arg}end|> List.unzip|> List.to_tuple
end

大多数操作发生在for语句中。本质上,我们经过了每个变量输入的AST片段,然后使用Macro.var/2函数计算临时名称(引用的argX),它能将一个原子变换成一个名称与其相同的引用的变量。Macro.var/2的第二个参数确保变量是卫生的。尽管我们将arg1,arg2,…变量注入到调用者上下文中,但调用者不会看到这些变量。事实上,deftraceable的用户可以自由地使用这些名称作为一些局部变量,不会干扰我们的宏引入的临时变量。

最后,在语境结束时,我们返回一个由temp的名称和引用的完整模式——(例如_ = arg10 = arg2)所组成的元组。在最后使用unzipto_tuple确保了decorate_args{arg_names, decorated_args}的形式返回结果。

有了decorated_argshelper,我们可以传递输入参数,获得修饰好的值,包含临时变量的名称。现在我们需要将这些修饰好的参数插入函数的头部,替换掉原始的参数。特别地,我们必须执行以下步骤:

  1. 递归遍历输入函数头的AST。

  2. 查找指定函数名和参数的位置。

  3. 将原始(输入)参数替换为修饰好的参数的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. Walk recursively through the ASThead = Macro.postwalk(head,# This lambda is called for each element in the input AST and# has a chance of returning alternative ASTfn# 2. Pattern match the place where function name and arguments are# specified({fun_ast, context, old_args}) when (fun_ast == fun_name and old_args == args_ast) -># 3. Replace input arguments with the AST of decorated arguments{fun_ast, context, decorated_args}# Some other element in the head AST (probably a guard)#   -> we just leave it unchanged(other) -> otherend)... # unchanged
end

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

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

这有点复杂,但它解决了我们的问题。以下是追踪宏的最终版本:

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# dynamically generate quoted identifierarg_name = Macro.var(:"arg#{index}", __MODULE__)# generate AST for patternX = argXfull_arg = quote dounquote(arg_ast) = unquote(arg_name)end{arg_name, full_arg}end|> List.unzip|> List.to_tupleend
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,把它分开,然后注入一些自定义的代码,这是可能的,而且不是非常复杂。缺点就是,生成宏的代码变得越来越复杂,并且更难分析。

本章到此结束。下一章我将讨论现场生成代码。

Copyright 2014, Saša Jurić. This article is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
The article was first published on the old version of the Erlangelist site.
The source of the article can be found here.

[elixir! #0007] [译] 理解Elixir中的宏——part.5 重塑AST by Saša Jurić相关推荐

  1. 理解OpenCV中的宏定义 CVAPI(函数返回类型)

    图像处理开发需求.图像处理接私活挣零花钱,请加微信/QQ 2487872782 图像处理开发资料.图像处理技术交流请加QQ群,群号 271891601 读代码时看到语句 CVAPI(IplImage* ...

  2. [译]理解js中的event loop

    之前看的文,感觉不太完整,于是找了篇详细的文章原文链接.翻译不正确的请指出,重在分享,如果有所收获就更好了. Javascript是如何异步和单线程的?简短的回答是javascript语言是单线程的, ...

  3. 如何理解计算机程序中的宏?

    计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式.解释器或编译器在遇到宏时会自动进行这一模式替换.对于编译语言,宏展开在编译时发生,进行宏展开的工具常被 ...

  4. [译] 理解 TypeScript 中 any 和 unknown

    译者: 在实际场景中, 很少看到有人在 Typescript 中使用 unknown , 使用unknown 可以保证类型安全,使用 any 则彻底放弃了类型检查 , 在很多情况下, 我们可以使用 u ...

  5. [译] 理解 NPM 5 中的 lock 文件

    本文讲的是[译] 理解 NPM 5 中的 lock 文件, 原文地址:Understanding lock files in NPM 5 原文作者:Jiří Pospíšil 译文出自:掘金翻译计划 ...

  6. 一篇文章理解JS中同步任务和异步任务以及宏任务与微任务的原理和执行机制

    前言: javascript是一种单线程编程语言, 一般来说它的执行顺序是按照从上到下执行,但是有些特殊情况则会改变这样的执行顺序,我们需要理解和掌握其中的原理,需要了解同步任务和异步任务以及宏任务和 ...

  7. 理解oracle中连接和会话

    理解oracle中连接和会话 1.  概念不同:概念不同: 连接是指物理的网络连接. 在已建立的连接上,建立客户端与oracle的会话,以后客户端与oracle的交互都在一个会话环境中进行. 2.   ...

  8. BBI综述:在微生物组研究中使用宏转录组

    在微生物组研究中使用宏转录组 Use of Metatranscriptomics in Microbiome Research https://doi.org/10.4137/BBI.S34610 ...

  9. BBI:Eran Elinav组综述在微生物组研究中使用宏转录组

    在微生物组研究中使用宏转录组 Use of Metatranscriptomics in Microbiome Research https://doi.org/10.4137/BBI.S34610 ...

最新文章

  1. 深度神经网络(DNN)的正则化
  2. canvas 实现图片局部模糊_Canvas模糊化处理图片、毛玻璃处理图片之stackblur.js
  3. python复制sheet_python excel sheet复制
  4. redis的事务不是原子性
  5. 如何在Android中使用OpenCV
  6. torch.cat() 函数用法
  7. Windows Server 2008 R2 Network Recovery
  8. Learning to rank 特征抽取
  9. VMware Converter迁移linux系统虚拟机
  10. TFTP协议下载服务器指定文件夹内的图片
  11. 软件工程保研成功率_软件工程专业保研还是工作?
  12. 学习java第15天
  13. 树莓派-MLX90614
  14. 重庆GDP超过了广州,纯属正常?
  15. 转载130个原文网站,原文链接:https://blog.csdn.net/qq_43901693/article/details/100606828
  16. Matlab矩阵和向量中的常用函数
  17. CSV格式文件向安卓小米手机中导入通讯录
  18. 电商平台满减活动需求分析
  19. 2023年安徽省中职网络安全跨站脚本攻击
  20. 18位身份证号验证算法的原理以及C#实现和在管理系统的应用

热门文章

  1. 最近想做个音乐共享的软件
  2. git track远程分支
  3. 深入探讨Varnish缓存命中率
  4. ASP.NET2.0 ReportingServices使用详解
  5. 使用eclipse创建Struts2项目
  6. Python3中__call__方法介绍
  7. 给动态生成的按钮添加ajax,Ajax/Javascript动态创建按钮的问题
  8. svn服务器搭建和使用 ubuntu_Ubuntu下 SVN 服务器搭建及使用
  9. JAVA写XML乱码问题_java 写 xml 中文乱码
  10. Java扫描配置文件的注解_详解Spring框架注解扫描开启之配置细节