(译) 理解 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ć.

是时候继续探索 Elixir 的宏了. 上次我介绍了一些关于宏的基本原理, 今天, 我将进入一个较少谈及的领域, 并讨论Elixir AST 的一些细节.

跟踪函数调用

到目前为止, 你只看到了接受输入 AST 片段并将它们组合在一起的基础宏, 并在输入片段周围或之间添加了一些额外的样板代码. 由于我们不分析或解析输入的 AST, 这可能是最干净(或最不 hackiest)的宏编写风格, 这样的宏相当简单且容易理解.

然而, 有时候我们需要解析输入的 AST 片段以获取某些特殊信息. 一个简单的例子是 ExUnit 的断言. 例如, 表达式 assert 1+1 == 2+2 会出现这个错误:

Assertion with == failed
code: 1+1 == 2+2
lhs:  1
rhs:  2

这个宏 assert 接收了整个表达式 1+1 == 2+2, 然后从中分出独立的表达式用来比较, 如果整个表达式返回 false, 则打印它们对应的结果. 所以, 宏的代码必须想办法将输入的 AST 分解为几个部分并分别计算子表达式.

更多时候, 我们调用了更复杂的 AST 变换. 例如, 你可以借助 ExActor 这样做:

defcast inc(x), state: state, do: new_state(state + x)

它会被转换为大致如下的形

def inc(pid, x) do:gen_server.cast(pid, {:inc, x})
enddef handle_cast({:inc, x}, state) do{:noreply, state+x}
end

assert 一样, 宏 defcast 需要深入分析输入的 AST 片段, 并找出每个子片段(例如, 函数名, 每个参数). 然后, ExActor 会执行一个精巧的变换, 将各个部分重组成一个更加复杂的代码.

今天, 我将想你展示构建这类宏的基础技术, 我也会在之后的文章中会将变换做得更复杂. 但在此之前, 我要请你认真考虑一下你的代码是否有有必要基于宏. 尽管宏十分强大, 但也有缺点.

首先, 就像之前我们看到的那样, 比起那些 "普通" 的运行时抽象 (函数, 模块, 协议), 宏的代码会很快地变得非常多. 你可以依赖 undocumented format (译注: 缺少文档解释, 寓意代码极其难以理解) 的 AST 来快速完成许多嵌套的 quote/unquoted 调用, 以及奇怪的模式匹配.

此外, 宏的滥用可能使你的客户端代码 (译注: 使用宏的代码) 极其难懂, 因为它将依赖于自定义的非标准习惯用法(例如 ExActordefcast). 这使得理解代码和了解底层究竟发生了什么变得更加困难.

从好的方面来看, 宏在删除样板代码时非常有用(正如 ExActor 示例所展示的那样), 并且具有访问运行时不可用的信息的能力(正如您应该从 assert 示例中看到的那样). 最后, 由于宏在编译期间运行, 因此可以通过将计算转移到编译时来优化一些代码.

因此, 肯定会有适合宏的情景, 您不应该害怕使用它们. 但是, 您不应该仅仅为了获得一些可爱的 dsl 式语法而选择宏. 在使用宏之前, 应该考虑是否可以依靠“标准”语言抽象(如函数、模块和协议)在运行时有效地解决问题.

探索 AST 结构

目前, 关于 AST 结构的文档不多. 然而, 在 shell 会话中可以很简单地探索和使用 AST, 我通常就是这样探索 AST 结构的.

例如, 这里有一个关于变量的 quoted

iex(1)> quote do my_var end
{:my_var, [if_undefined: :apply], Elixir}

在这里, 第一个元素代表变量的名称;第二个元素是上下文 Keyword 列表, 它包含了该 AST 片段的元数据(例如 imports 和 aliases). 通常你不会对上下文数据感兴趣;第三个元素通常代表 quoted 发生的模块, 同时也用于确保 quoted 变量的 hygienic. 如果该元素为 nil, 则该标识符是不 hygienic 的.

一个简单的表达式看起来包含了许多东西:

iex(2)> quote do a+b end
{:+, [context: Elixir, import: Kernel],[{:a, [if_undefined: :apply], Elixir}, {:b, [if_undefined: :apply], Elixir}]}

看起来可能很复杂, 但是如果我向你展示更高层次的表达模式, 就很容易理解了:

{:+, context, [ast_for_a, ast_for_b]}

在我们的例子中, ast_for_aast_fot_b 遵循着你之前所看到的变量的形状(如 {:a, [if_undefined: :apply], Elixir}). 一般, quoted 的参数可以是任意复杂的, 因为它们描述了每个参数的表达式. 事实上, Elixir AST 是一个简单 quoted expression 的深层结构, 就像我给你展示的那样.

让我们看一个关于函数调用的例子:

iex(3)> quote do div(5,4) end
{:div, [context: Elixir, import: Kernel], [5, 4]}

这类似于 quoted + 的操作, 我们知道 + 实际上是一个函数. 事实上, 所有二进制运算符都会像函数调用一样被 quoted.

最后, 让我们来看一个被 quoted 的函数定义:

iex(4)> quote do def my_fun(arg1, arg2), do: :ok end
{:def, [context: Elixir, import: Kernel],[{:my_fun, [context: Elixir],[{:arg1, [if_undefined: :apply], Elixir},{:arg2, [if_undefined: :apply], Elixir}]},[do: :ok]]}

看起来有点吓人, 但可以只看重要的部分来简化它. 事实上, 这种深层结构相当于:

{:def, context, [fun_call, [do: body]]}

fun_call 是一个函数调用的结构(正如之前你看过的那样).

如你所见, AST 背后通常有一些逻辑和意义. 我不会在这里写出所有 AST 的形状, 但会在 iex 中尝试你感兴趣的简单的结构来探索 AST. 这是一个逆向工程, 但不是火箭科学.

写一个 assert 宏

为了快速演示, 让我们编写一个简化版的 assert 宏. 这是一个有趣的宏, 因为它重新定义了比较操作符的含义. 通常, 当你写下 a == b 表达式时, 你会得到一个布尔结果. 但是, 当将此表达式给 assert 宏时, 如果表达式的计算结果为 false, 则会打印详细的输出.

我将从简单的部分开始, 首先在宏里只支持 == 运算符. 可以知道, 我们调用 assert expected == required 时, 等同于调用 assert(expect == required), 这意味着我们的宏接收到一个表示比较的引用片段. 让我们来探索这个比较表达式的 AST 结果:

iex(1)> quote do 1 == 2 end
{:==, [context: Elixir, import: Kernel], [1, 2]}iex(2)> quote do a == b end
{:==, [context: Elixir, import: Kernel],[{:a, [if_undefined: :apply], Elixir}, {:b, [if_undefined: :apply], Elixir}]}

所以我们的结构本质上是 {:==, context, [quoted_lhs, quoted_rhs]}. 如果你记住了前几个系列中所演示的例子, 那么就不会感到意外, 因为我提到过二进制运算符是作为 2 个参数的函数被 quoted 的.

知道了 AST 的形状, 实现这个宏就很简单:

defmodule Assertions dodefmacro assert({:==, _, [lhs, rhs]} = expr) doquote doleft = unquote(lhs)right = unquote(rhs)result = (left == right)unless result doIO.puts "Assertion with == failed"IO.puts "code: #{unquote(Macro.to_string(expr))}"IO.puts "lhs: #{left}"IO.puts "rhs: #{right}"endresultendend
end

第一个有趣的事情发生在第 2 行. 注意我们是如何对输入表达式进行模式匹配的, 希望它符合某种结构. 这完全没问题, 因为宏是函数, 这意味着您可以依赖于模式匹配、guards(守卫), 甚至有多子句宏. 在我们的例子中, 我们依靠模式匹配将(被 quoted 的)比较表达式的每一边带入相应的变量.

然后, 在 quoted 的代码中, 我们通过分别计算左边和右边重新解释 == 操作(第 4 行和第 5 行), 然后是整个结果(第 7 行). 最后, 如果结果为假, 我们打印详细信息(第 9-14 行).

来试一下:

iex(1)> defmodule Assertions do ... end
iex(2)> import Assertionsiex(3)> assert 1+1 == 2+2
Assertion with == failed
code: 1 + 1 == 2 + 2
lhs: 2
rhs: 4
false

将代码实现通用化

将之前的代码用到其他的运算操作符并不困难:

defmodule Assertions dodefmacro assert({operator, _, [lhs, rhs]} = expr)when operator in [:==, :<, :>, :<=, :>=, :===, :=~, :!==, :!=, :in] doquote doleft = unquote(lhs)right = unquote(rhs)result = unquote(operator)(left, right)unless result doIO.puts("Assertion with #{unquote(operator)} failed")IO.puts("code: #{unquote(Macro.to_string(expr))}")IO.puts("lhs: #{left}")IO.puts("rhs: #{right}")endresultendend
end

这里只有一点点变化. 首先, 在模式匹配中, 硬编码(hard code) :== 被变量 operator 取代了(第 2 行).

我还引入(实际上, 是从 Elixir 源代码中复制粘贴了)guard 语句指定了宏能处理的运算符集(第 3 行). 这个检查有一个特殊原因. 还记得我之前提到的, quoted a + b(或任何其它的二进制操作)的形状等同于引用 fun(a, b). 因此, 没有这些 guard 语句, 任何双参数的函数调用都会在我们的宏中结束, 这可能是我们不想要的. 使用这个 guard 语句能将输入限制在已知的二进制运算符中.

有趣的事情发生在第 9 行. 在这里我使用了 unquote(operator)(left, right) 来对操作符进行简单的泛型分派. 你可能认为我可以使用 left unquote(operator) right 来替代, 但它并不能运算. 原因是 operator 变量保存的是一个原子(如:==). 因此, 这个天真的 quoted 会产生 left :== right, 这甚至不符合 Elixir 的语法规定.

记住, 在 quote 时, 我们不组装字符串, 而是组装 AST 片段. 所以, 当我们想生成一个二进制操作代码时, 我们需要注入一个正确的 AST, 它(如前所述)与双参数的函数调用相同. 因此, 我们可以简单地使用函数调用的方式 unquote(operator)(left, right).

这一点讲完了, 今天的这一章也该结束了. 它有点短, 但略微复杂些. 下一章 《(译) Understanding Elixir Macros, Part 4 - Diving Deeper》, 我将深入 AST 解析的话题.

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

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

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

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

相关文章

【机器学习】“强化机器学习模型:Bagging与Boosting详解“

1. 引言 在当今数据驱动的世界里&#xff0c;机器学习技术已成为解决复杂问题和提升决策制定效率的关键工具。随着数据的增长和计算能力的提升&#xff0c;传统的单一模型方法已逐渐无法满足高精度和泛化能力的双重要求。集成学习&#xff0c;作为一种结合多个学习算法以获得比…

2024-04-03 问AI: 在深度学习中,什么是训练中的epoch?

文心一言 在深度学习中&#xff0c;一个“epoch”指的是整个数据集被模型完整地遍历和学习一次的过程。换句话说&#xff0c;当模型已经看过数据集中的所有样本一次时&#xff0c;我们就说已经完成了一个epoch。 理解epoch的概念对于训练深度学习模型非常重要。在训练过程中&…

Linux下docker运行python

前言 本机开发环境众多&#xff0c;python版本都好多个&#xff0c;虽然可以通过conda管理多个虚拟环境&#xff0c;但还是不能像容器那样进行进程间的隔离。于是打算试下docker下运行python&#xff0c;而且生产环境很多时候也是用容器来跑应用&#xff0c;环境统一、方便扩容…

hive的使用(本地数据上传到idea)

1.首先第一步是先启动hive&#xff0c;hive的启动指令如下 nohup hiveserver2 & 2.然后我们进入idea中 这里我们使用的是idea里的Apache Hive的插件&#xff0c;进行配置&#xff0c;等我们跟moba连接好后&#xff0c;就可以进行数据的导入了。 hive的sql和mysql里的sql语…

vmvare ubuntu 拖拽复制

目录 先是弹框报错&#xff1a; 但是拖拽到文件夹还是红色的禁止符号&#xff0c;解决方法 在VMware中安装新版Ubuntu后&#xff0c;无法跨虚拟机复制粘贴和拖拽文件的解决方法 先是弹框报错&#xff1a; Ubuntu 22.04 drag and drop is not supported 解决方法&#xff1a;…

常见的sql优化策略

常见的 SQL 优化策略包括&#xff1a; 使用索引&#xff1a; 索引可以加速 SQL 查询的速度&#xff0c;特别是对于经常用于筛选、排序和连接的列。确保在经常查询的列上创建合适的索引&#xff0c;以提高查询性能。 合理设计数据库结构&#xff1a; 设计合理的数据库结构可以提…

ubuntu安装sublime3并设置中文

安装Sublime Text 3 在Ubuntu上安装Sublime Text 3可以通过以下步骤进行&#xff1a; 打开终端。 导入Sublime Text 3的GPG密钥&#xff1a; wget -qO- https://download.sublimetext.com/sublimehq-pub.gpg | sudo apt-key add - 添加Sublime Text 3的存储库&#xff1a; …

蓝奏云直链获取在线解析网站源码

源码简介 蓝奏云直链获取在线解析网站源码 蓝奏云链接解析 本地API接口 支持有无密码和短期直链和永久直链&#xff0c;同时还可以显示文件名和大小。 这个解析器无需数据库即可搭建&#xff0c;API接口已经本地化&#xff0c;非常简单易用。 安装环境 php5.6 搭建教程 …

WPF 行为

WPF 行为 一、前言 行为是一类事物的共同特征&#xff0c;可以向用户界面控件添加功能&#xff0c;而无需将其子类化。 功能是在行为类中实现的&#xff0c;并附加到控件上&#xff0c;就像它本身就是控件的一部分。 比如在鼠标进入/离开控件时&#xff0c;表现出不同的现象…

excel wps中编码格式转换

EXCEL报表&#xff1a;另存为CSV格式&#xff0c;转换成UTF-8编码 - 简书 (jianshu.com) 经验证管用

Oracle数据库安全管理与数据加密技术

一、引言 数据安全性的重要性 在现代社会中&#xff0c;信息安全已经成为国家安全和企业安全的重要组成部分。随着人们对数字数据的使用越来越频繁&#xff0c;保护数字信息的安全性和完整性变得越来越重要。对于企业和个人用户来说&#xff0c;数据安全比任何时候都要重要&a…

Flask Python:如何获取不同请求方式的参数

目录 前言 1. 获取GET请求中的查询参数 2. 获取POST请求中的表单数据 3. 获取JSON数据 总结 前言 在使用Flask开发Web应用时&#xff0c;我们经常需要获取不同请求方式的参数。Flask提供了多种方式来获取不同请求方式的参数&#xff0c;包括GET请求中的查询参数、POST请求…

Node.js环境调用百度智能云(百度云)api鉴权认证三步走

方式一 :Postman脚本的方式生成v1版本的认证字符串 Postman脚本下载 下载Postman pre-request Script 设置 Authorization 示例脚本 方式二&#xff1a;在线签名工具生成 (试用于验证编程字符串签名是否有错误) 签名计算工具 https://cloud.baidu.com/signature/index.html …

postgis 建立路径分析,使用arcmap处理路网数据,进行拓扑检查

在postgresql+postgis上面,对路网进行打断化简,提高路径规划成功率。 一、创建空间库以及空间索引 CREATE EXTENSION postgis; CREATE EXTENSION pgrouting; CREATE EXTENSION postgis_topology; CREATE EXTENSION fuzzystrmatch; CREATE EXTENSION postgis_tiger_geocoder;…

掌握机器学习新星:使用Python和Scikit-Learn进行图像识别

正文&#xff1a; 随着智能手机和社交媒体的普及&#xff0c;图像数据的生成速度比以往任何时候都快。为了自动化处理这些数据&#xff0c;我们需要强大的图像识别系统。机器学习提供了一种有效的方法来识别和分类图像中的对象。Scikit-Learn是一个流行的Python库&#xff0c;它…

elementui 左侧或水平导航菜单栏与main区域联动

系列文章目录 一、elementui 导航菜单栏和Breadcrumb 面包屑关联 二、elementui 左侧导航菜单栏与main区域联动 三、elementui 中设置图片的高度并支持PC和手机自适应 四、elementui 实现一个固定位置的Pagination&#xff08;分页&#xff09;组件 文章目录 系列文章目录…

R语言数据挖掘:随机森林(1)

数据集heart_learning.csv与heart_test.csv是关于心脏病的数据集&#xff0c;heart_learning.csv是训练数据集&#xff0c;heart_test.csv是测试数据集。要求&#xff1a;target和target2为因变量&#xff0c;其他诸变量为自变量。用决策树模型对target和target2做预测&#xf…

zookeeper监听集群节点的实现zkclient组件实现方案(Java版)

ZooKeeper Watcher 机制 client 向zookeeper 注册监听client注册的同时会存储一个WatchManager对象向zookeeper发生改变则notification client 并发送一个WatchManager对象,然后client再更新该对象 package com.jacky.zk.demo;import org.I0Itec.zkclient.IZkChildListener;…

MacOS 14 搭建 PHP7.4 + Xdebug开发环境

摘要 项目使用的技术栈&#xff1a;PHP 7.4.33、ThinkPHP 5.1.27、Redis、MySQL 由于MacOS预装的PHP版本较高&#xff0c;所以需要降级 IDE 使用 PhpStorm 2023.2.3 1. 安装PHP 7.4.33 MacOS 14中的brew版本比较高&#xff0c;没有低版本的PHP brew install shivammathur/p…

如何保证缓存和数据库一致性----缓存双删

如何保证缓存和数据库的一致性 a.缓存双删&#xff1a;修改前删&#xff0c;修改后删&#xff0c;可以用aop去实现&#xff08;不能保证一致性&#xff09; b.或者根据业务场景去看&#xff0c;需不需要满足强一致性 c.对强一致性要求特别高需要加业务锁&#xff0c;只要有修改…