发布于 2026-01-06 3 阅读
0

元编程:从 C 预处理到 Elixir 宏 C/C++ Ruby Elixir ❤️ 结论

元编程:从 C 预处理到 Elixir 宏

C/C++

红宝石

灵药❤️

结论

开发者对元编程可谓又爱又恨。一方面,它是构建可重用代码的强大工具;另一方面,它也可能很快变得难以理解和维护。

我喜欢把它比作盐。很多时候它都很有用,但如果用量过多,就会做出不好吃的菜。

此外,大剂量服用其中任何一种都可能导致血压升高。😅

然而,元编程自诞生之初以来已经取得了长足的进步。虽然我仍然尽量避免过度使用它,但它变得越来越有用,也越来越易于​​使用。让我们来看看它是如何演变的。

C/C++

如果我们回到几十年前,回到编程语言更接近底层硬件的时代,C/C++ 预处理器是我们进行类似元编程的少数选择之一。

这个预处理器正如其名:它是一个解析器,能够遍历 C 代码,处理特定的定义(例如关键字 ` and` 和#define`or` #if),并将最终版本的 C 代码输出给编译器。这个最终版本会根据某些标准而改变。它看起来大概是这样的:

#define FOO 1

#if FOO == 1
#define MSG "Hello, World"
#else
#define MSG "Goodbye, World"
#endif

#include <stdio.h>

int main() {
  printf(MSG);
end
Enter fullscreen mode Exit fullscreen mode

这个程序会"Hello, World"一直输出“”。正如你可能猜到的,将 FOO 的定义改为 0,然后重新编译程序,它就会输出“” "Goodbye, World"

这些预处理器指令通常用于创建针对特定平台或架构的代码。例如,您可以为编译到 Windows 系统和 Linux 系统的程序设置不同的行为。生成的两个二进制文件将仅包含与特定平台相关的代码,因此无需执行运行时检查。这通常可以显著节省存储空间并提升运行时性能。

然而,如果你有任何 C 语言经验,你就会知道它本身就很危险。如果在此基础上添加大量的预处理行为,它很快就会变得难以管理。因此,大多数情况下,不建议将其用于大型配置。

红宝石

随着技术的进步和更高级脚本语言的出现,也出现了创建更复杂编程风格的可能性。尤其是在 Ruby 中,元编程被证明是一项强大但又令人望而生畏的特性。

Ruby 的工作原理基于这样的理念:代码只不过是一串文本,由 Ruby 环境解释和执行。

由于 Ruby 是运行时解释型语言,因此无需预先编译整个代码库。Ruby 允许您动态地为类定义实例方法。

此外,由于 Ruby 类和实例的内部构造方式,您甚至可以为单个实例而不是整个类定义方法!

PS:更多关于 Ruby 类的信息请点击此处阅读。

class Foo
  def hello1
    puts "Hello from a regular method"
  end

  [:hello2, :hello3].each do |f|
    define_method f do
      puts "Hello from a dynamically-defined #{f} method"
    end
  end
end

foo = Foo.new

foo.define_singleton_method(:hello4) { puts "Hello only from this instance of Foo" }

foo.hello1
foo.hello2
foo.hello3
foo.hello4
Enter fullscreen mode Exit fullscreen mode

Ruby 在编辑现有代码方面也相当宽松,即使是来自标准库的代码也不例外。以下是有效的 Ruby 代码:

array = [1, 2, 3]

# will print out 3
puts array.size

class Array
  def size
    "Hello"
  end
end

# will now print out "Hello"
puts array.size
Enter fullscreen mode Exit fullscreen mode

千万别这么做!这很可能会破坏你的程序,而且总体来说也是一种不好的做法。

最后但同样重要的是,Ruby 提供了一些强大的方法来处理意外的函数调用,例如method_missing回调函数:

array = [1, 2, 3]

class Array
  def method_missing(method, *args)
    puts "#{method} method not found"

    if method == :sise then
      puts "Did you intend to type size instead?"
    end
  end
end

puts array.sise
Enter fullscreen mode Exit fullscreen mode

总的来说,这些能力在我第一次接触它们时,对我来说意义非凡。它们让我能够以一种全新的方式思考我的代码库,并在这一过程中对其进行改进。

不过,也存在一些问题。俗话说得好:能力越大,责任越大。

一些 Ruby 库滥用了这些元编程机制,创建了自己的领域特定语言。长此以往,这种过度使用会导致与 C++ 时代类似的问题:代码库难以维护和理解。

在我看来,Elixir 这次又朝着正确的方向迈出了一步……

灵药❤️

Elixir 将元编程以更强大的方式内置于语言核心之中。Ruby 允许你动态定义方法,甚至可以生成字符串并将其作为代码执行(这种老eval方法我们都讨厌),而 Elixir 则允许你直接修改抽象语法树 (AST)。

这是通过quote关键词实现的:

iex> expr = quote do
  "Hello, " <> "World"
end
Enter fullscreen mode Exit fullscreen mode

尝试运行上面的代码,你会发现字符串连接操作并没有直接执行。最终得到的不是最终的字符串,而是一个描述代码的抽象语法树(AST)表达式:

{:<>, [context: Elixir, import: Kernel], ["Hello, ", "World"]}
Enter fullscreen mode Exit fullscreen mode

熟悉波兰表示法的人很快就会发现,这与上面的字符串连接代码等效。因此,通过引用一段代码,你可以得到该代码的抽象语法树(AST)描述,然后可以在代码库的其他部分中使用它。

然后,你就可以像分析数据结构(它实际上就是……抽象语法树)一样分析你的代码,并执行相应的操作来转换它:

我们稍微修改一下:

iex> expr = quote do
  "Hello, " <> name
end
Enter fullscreen mode Exit fullscreen mode

现在我们的表达式使用了一个动态名称。但是,这个名称是从哪里来的呢?
我们没有在任何地方定义这个变量,但它的语法仍然是正确的:

{:<>, [context: Elixir, import: Kernel], ["Hello, ", {:name, [], Elixir}]}
Enter fullscreen mode Exit fullscreen mode

但是,它将无法执行,我们可以使用以下命令进行测试Code.eval_quoted/3

iex> Code.eval_quoted(expr)
** (CompileError) nofile:1: undefined function name/0
    (elixir) lib/code.ex:590: Code.eval_quoted/3
    test.ex:5: (file)
Enter fullscreen mode Exit fullscreen mode

现在我们来创建第二个 AST 定义:

definition = quote do
  name = "Miguel"
end
Enter fullscreen mode Exit fullscreen mode

第二个表达式定义定义了一个名为 的变量name。但是,请记住,我们并没有定义任何值,只是为该操作创建了抽象语法树 (AST)。

我们可以将这两个表达式合并成一个表达式:

final_code = quote do
  unquote(definition)
  unquote(expr)
end
Enter fullscreen mode Exit fullscreen mode

这样做的结果和我们输入以下内容的结果是一样的:

name = "Miguel"
"Hello, " <> name
Enter fullscreen mode Exit fullscreen mode

但是请注意,我们始终没有放弃 Elixir 的语法和规则。我们编写的 Elixir 代码本身就是 Elixir 代码!

Elixir 核心内部大量使用了这种机制。每当你定义一个函数或一个简单的 if 语句时,你实际上都在执行宏,这些宏会根据宏的类型修改代码的抽象语法树 (AST),使你的代码能够适应这些类型。说到这里……

Elixir 的宏

Elixir 的许多特性都是用宏编写的。许多常用的运算符都可以用宏重写。例如,我们来unless定义一下运算符(它已经存在于语言的核心中):

defmodule Foo do
  defmacro custom_unless(condition, do: do_clause, else: else_clause) do quote do
      if !unquote(condition) do
        unquote(do_clause)
      else
        unquote(else_clause)
      end
    end
  end

  defmacro custom_unless(condition, do: do_clause) do
    quote do
      Foo.custom_unless(unquote(condition), do: unquote(do_clause), else: nil)
    end
  end
end

defmodule Bar do
  require Foo

  Foo.custom_unless true, do: IO.puts("not true"), else: IO.puts("true")
end
Enter fullscreen mode Exit fullscreen mode

我们的custom_unless宏接收一个布尔值作为参数。在宏内部,我们会检查条件的相反情况(即,根据该条件运行给定的代码抽象语法树,并将结果取反)。然后,根据结果,执行针对 `is`do或 `is`子句的 `is` 抽象语法树。else

然而,Elixir 的乐趣在于,即使是像if子句这样的基本结构也经常使用宏来构建,因此我们可以更好地将宏嵌入到语言中。换句话说,定义宏之后,以下代码也能正常运行:

defmodule Bar
  # importing instead of requiring allows us to call the macro directly,
  # without the Foo. prefix
  import Foo

  custom_unless true do
    IO.puts("not true")
  else
    IO.puts("true")
  end
end
Enter fullscreen mode Exit fullscreen mode

之所以有效,是因为 Elixir 中多行 if/else 代码块的解释本质上只是以下语句的语法糖:

if condition do: something, else: something_else
Enter fullscreen mode Exit fullscreen mode

结论

希望本文能帮助读者了解宏在过去是如何演变的,特别是对于那些可能不了解 Elixir 语言全部功能及其历史的 Elixir 开发人员而言。

如果你想定期收到⚗️灵药炼金术的内容,请订阅,即可将下一集炼金术直接发送到你的邮箱。

本文由特约作者Miguel Palhas撰写。Miguel 是@subvisual 的专业过度设计工程师,也是@rubyconfpt@MirrorConf的组织者

文章来源:https://dev.to/appsignal/metaprogramming-from-c-preprocessing-to-elixir-macros-6a1