通常,我们认为程序是通过操纵数据来实现某种结果的东西。
但是数据是什么?
我们可以将程序本身视为数据吗?
在今天的文章中,我们将借助 Elixir(一种充满元编程的编程语言)深入这个兔子洞。
我将向您介绍 Elixir 中的元编程,并向您展示如何创建用于定义柯里化函数的宏。
什么是元编程?
元编程是编写操纵程序的程序。这是一个宽泛的术语,可以包括编译器、解释器和其他类型的程序。
在这篇文章中,我们将重点介绍 Elixir 中的元编程,其中涉及宏和编译时代码生成。
Elixir 中的元编程
要了解 Elixir 中的元编程工作原理,了解编译器工作原理非常重要。在编译过程中,每个计算机程序都会转换为抽象语法树 (AST) - 一种树状结构,使计算机能够理解程序的内容。
快速数学运算。
在 Elixir 中,AST 的每个节点(原始值除外)都是一个由三部分组成的元组:函数名称、元数据和函数参数。
Elixir 让我们通过引用访问这个内部 AST 表示。
iex(1)> quote do 2 + 2 - 1 end
{:-, [context: Elixir, import: Kernel],
[{:+, [context: Elixir, import: Kernel], [2, 2]}, 1]}
我们可以使用宏来修改这些 AST,宏是在编译时执行的从 AST 到 AST 的函数。
您可以使用宏来生成模板代码,创建新的语言特性,甚至构建领域特定语言(DSL)。
其实在 Elixir 中我们经常用到的很多语言结构,比如 def、defmodule、if 等都是宏。此外,很多流行的库,比如 Phoenix、Ecto、Absinthe 等都大量使用了宏来打造便捷的开发者体验。
以下是来自文档的一个 Ecto 查询示例:
query = from u in "users",
where: u.age > 18,
select: u.name
Elixir 中的元编程是一个强大的工具。它在表达能力上接近 LISP(原始元编程鼓),但在抽象性上更高,允许您仅在需要时深入研究 AST。换句话说,Elixir 基本上是 LISP,但可读性更高。
开始使用
那么,我们该如何引导这一巨大的力量呢?
虽然元编程可能比较棘手,但在 Elixir 中开始使用元编程却相当容易。你只需要知道三件事。
引用
引述结束
定义宏
代码 -> AST
外引号 -> 内引号
AST->AST
参考
将 Elixir 代码转换为其内部 AST 表示。
您可以将正则表达式和引用表达式之间的区别视为两个不同请求之间的区别:
iex(1)> 2 + 2
4
iex(2)> quote do 2 + 2 end
{:+, [context: Elixir, import: Kernel], [2, 2]}
quote 使得编写 Elixir 宏变得轻而易举,因为我们不需要手动生成或编写 AST。
删除引号
但是如果我们想访问 quote 里面的变量怎么办?解决方案是。
unquote 功能类似于字符串插值,使您能够将变量从其周围环境拉入引用块中。
在 Elixir 中它看起来是这样的:
iex(1)> two = 2
2
iex(2)> quote do 2 + 2 end
{:+, [context: Elixir, import: Kernel], [2, 2]}
iex(3)> quote do two + two end
{:+, [context: Elixir, import: Kernel],
[{:two, [], Elixir}, {:two, [], Elixir}]}
iex(4)> quote do unquote(two) + unquote(two) end
{:+, [context: Elixir, import: Kernel], [2, 2]}
如果我们不对 two 取消引用,我们将获得 Elixir 对某个未赋值的变量 2 的内部表示。如果我们取消引用它,我们可以在引用的块内访问该变量。
定义宏
宏是从 AST 到 AST 的函数。
例如,假设我们想要创建一种新的表达式类型来检查数字的奇异性。
我们可以使用 defmacro、quote 和 unquote 在短短几行中为此创建一个宏:
defmodule My do
defmacro odd(number, do: do_clause, else: else_clause) do
quote do
if rem(unquote(number), 2) == 1, do: unquote(do_clause), else: unquote(else_clause)
end
end
end
iex(1)> require My
My
iex(2)> My.odd 5, do: "is odd", else: "is not odd"
"is odd"
iex(3)> My.odd 6, do: "is odd", else: "is not odd"
"is not odd"
何时应该使用元编程?
“规则 1:不要编写宏” - Chris McCord,Elixir 中的元编程
虽然元编程是一个很好的工具,但应该谨慎使用。
宏会使调试更加困难,并增加整体复杂性。您应该只在必要时使用它们——当您遇到常规函数无法解决的问题时,或者当有大量幕后管道需要隐藏时。
但是,如果使用得当,它们可以带来巨大的好处。为了了解它们如何改善开发人员的生活,让我们看看 Elixir 的主要 Web 框架 Phoenix 中的一些真实示例。
如何在 Phoenix 中使用宏
在接下来的章节中,我们将以分析新创建的 Phoenix 项目的路由器子模块为例,说明如何在 Elixir 中使用宏。
使用
如果你查看任何 Phoenix 文件的顶部,你很可能会看到一个 use 宏。我们的路由器子模块有一个:
defmodule HelloWeb.Router do
use HelloWeb, :router
这个宏的扩展是
require HelloWeb
HelloWeb.__using__(:router)
require 要求编译其宏Elixir,以便它们可以在模块中使用。但它是什么?正如您可能已经猜到的,它是另一个宏!
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
在我们的示例中,这个宏调用 HelloWeb 模块中的路由器函数。
def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
end
end
路由器导入两个模块并启动另一个宏。__using__
如您所见Elixir Elixir中的元编程(附代码示例),这隐藏了很多东西Elixir,这可能是好事也可能是坏事。但它也为我们提供了神奇的 HelloWeb 用法Elixir Elixir中的元编程(附代码示例),:router 已为我们需要的快速 webdev 操作做好了一切准备。
管道
现在,考虑以下用法:
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end