作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
贝多的头像

Will Beddow

Will是专门设计复杂性能系统的全栈开发人员. 他最近的工作包括金融科技系统设计, 云编排程序, 以及大型企业软件系统.

Expertise

Previously At

波士顿咨询集团
Share

每当我想到解释宏的最佳方式, 我记得我刚开始编程时写的一个Python程序. 我无法按照我想要的方式组织它. 我必须调用许多稍有不同的函数,代码变得很麻烦. 我一直在寻找的东西——尽管当时我还不知道 metaprogramming.

metaprogramming (noun)

程序将代码视为数据的任何技术.

我们可以构建一个示例来演示我在Python项目中遇到的相同问题,想象我们正在为宠物主人构建一个应用程序的后端. 使用图书馆里的工具, pet_sdk,我们编写Python帮助宠物主人购买猫粮:

import pet_sdk

cats = pet_sdk.get_cats()
print(f"Found {len(cats)} cats "!")
for cat in cats:
    pet_sdk.order_cat_food(猫,金额=猫.food_needed)
片段1:订购猫粮

在确认代码工作后, 我们继续为另外两种宠物(鸟和狗)实现相同的逻辑。. 我们还增加了预约兽医的功能:

一个SDK,可以给我们关于宠物的信息-不幸的是,功能 are slightly different for each pet
import pet_sdk

#把所有的鸟、猫和狗分别放进系统
birds = pet_sdk.get_birds()
cats = pet_sdk.get_cats()
dogs = pet_sdk.get_dogs()

for cat in cats:
    打印(f)“正在检查cat {cat.name}")

    if cat.hungry():
        pet_sdk.order_cat_food(猫,金额=猫.food_needed)
    
    cat.clean_litterbox()

    if cat.sick():
        Available_vets = pet_sdk.find_vets(动物=“猫”)
        if len(available_vets) > 0:
            [0] [au:]
            vet.book_cat_appointment (cat)

for dog in dogs:
    打印(f)“检查狗的信息{狗.name}")

    if dog.hungry():
        pet_sdk.order_dog_food(狗,金额=狗.food_needed)
    
    dog.walk()

    if dog.sick():
        Available_vets = pet_sdk.find_vets(动物=“狗”)
        if len(available_vets) > 0:
            [0] [au:]
            vet.book_dog_appointment(狗)

for bird in birds:
    打印(f)“检查鸟的信息{鸟.name}")

    if bird.hungry():
        pet_sdk.order_bird_food(鸟,数量=鸟.food_needed)
    
    bird.clean_cage()

    if bird.sick():
        Available_vets = pet_sdk.find_birds(动物=“鸟”)
        if len(available_vets) > 0:
            [0] [au:]
            vet.book_bird_appointment(鸟)
Snippet 2: Order Cat, Dog, and Bird Food; Book Vet Appointment

最好将代码片段2的重复逻辑压缩成一个循环, 所以我们开始重写代码. 我们很快意识到,由于每个函数的命名不同,我们无法确定是哪个函数.g., book_bird_appointment, book_cat_appointment)在循环中调用:

import pet_sdk

All_animals = pet_sdk.Get_birds () + pet_sdk.Get_cats () + pet_sdk.get_dogs()

对于all_animals中的animal:
    # What now?
片段3:What Now?

Let’s imagine a Python的涡轮增压版 我们可以编写程序,自动生成我们想要的最终代码——我们可以灵活地编写程序, easily, 并且流畅地操作我们的程序,就好像它是一个列表, data in a file, 或任何其他通用数据类型或程序输入:

import pet_sdk

对于["cat", "dog", "bird"]中的动物:
    animals = pet_sdk.get_{animal}s() #当动物是“猫”,这
                                      # would be pet_sdk.get_cats()

    动物中的动物:
        pet_sdk.order_{动物}_food(动物,数量=动物.food_needed)
        当动物是“狗”时,这就是
        # pet_sdk.order_dog_food(狗,金额=狗.food_needed)
代码片段4:turbpython:一个虚程序

这是a的一个例子 macro的语言版本 Rust, Julia, or C,但Python除外.

这个场景是一个很好的例子,说明编写一个能够修改和操作自己代码的程序是多么有用. 这正是宏的作用所在, 这是一个更大的问题的众多答案之一:我们如何让一个程序反省自己的代码, 将其作为数据处理, 然后根据自省采取行动?

Broadly, 所有能够完成这种自省的技术都属于“元编程”这个总称.元编程是编程语言设计中一个丰富的子领域, 它可以追溯到一个重要的概念: code as data.

反思:为Python辩护

你可能会指出这一点, 尽管Python可能不提供宏支持, 它提供了许多其他方法来编写此代码. 例如,这里我们使用the isinstance() 方法来识别我们的类 animal 变量是一个实例,并调用相应的函数:

一个SDK,可以给我们关于宠物的信息-不幸的是,功能
#略有不同

import pet_sdk

def process_animal(动物):
    如果isinstance(animal, pet_sdk.Cat):
        Animal_name_type = "猫"
        Order_food_fn = pet_sdk.order_cat_food
        care_fn = animal.clean_litterbox 
    El如果isinstance(animal, pet_sdk . exe.Dog):
        Animal_name_type = "狗"
        Order_food_fn = pet_sdk.order_dog_food
        care_fn = animal.walk
    El如果isinstance(animal, pet_sdk . exe.Bird):
        Animal_name_type = "鸟"
        Order_food_fn = pet_sdk.order_bird_food
        care_fn = animal.clean_cage
    else:
        抛出TypeError("无法识别的动物。!")
    
    打印(f)“检查{animal_name_type} {animal . type}的信息。.name}")
    if animal.hungry():
        order_food_fn(动物,数量=动物.food_needed)
    
    care_fn()

    if animal.sick():
        Available_vets = pet_sdk.find_vets(动物= animal_name_type)
        if len(available_vets) > 0:
            [0] [au:]
            我们还得再检查一下它是什么类型的动物
            如果isinstance(animal, pet_sdk.Cat):
                vet.book_cat_appointment(动物)
            El如果isinstance(animal, pet_sdk . exe.Dog):
                vet.book_dog_appointment(动物)
            else:
                vet.book_bird_appointment(动物)


All_animals = pet_sdk.Get_birds () + pet_sdk.Get_cats () + pet_sdk.get_dogs()
对于all_animals中的animal:
    process_animal(动物)
代码片段5:一个惯用的例子

我们称这种类型为元编程 reflection,我们稍后再来讨论. Snippet 5的代码仍然有点麻烦,但对于程序员来说,编写起来比编写代码容易 Snippet 2在这个过程中,我们对列出的每一种动物重复上述逻辑.

Challenge

Using the getattr method,修改上述代码以调用相应的 order_*_food and book_*_appointment 动态功能. 这无疑会降低代码的可读性, 但是如果你很了解Python, 值得考虑如何使用它 getattr instead of the isinstance 函数,并简化代码.


同象性:Lisp的重要性

一些编程语言,比如 Lisp,将元编程的概念提升到另一个层次 homoiconicity.

homoiconicity (noun)

程序设计语言的一种特性,即在代码和程序所依据的数据之间没有区别.

Lisp, created in 1958, 最古老的同义符号语言和第二古老的高级编程语言. 它的名字来自“列表处理器”,“Lisp是计算机领域的一场革命,它深刻地影响了计算机的使用和编程方式. 很难夸大Lisp对编程的影响有多么根本和独特.

Emacs是用Lisp编写的,Lisp是唯一漂亮的计算机语言. Neal Stephenson

Lisp的诞生只比FORTRAN晚一年, 在打孔卡和军用电脑充斥整个房间的时代. 然而,程序员今天仍然使用Lisp来编写新的、现代的应用程序. Lisp的主要创造者, John McCarthy他是人工智能领域的先驱. For many years, Lisp是人工智能的语言, 研究人员非常看重动态重写自己代码的能力. 今天的人工智能研究以神经网络和复杂的统计模型为中心, 而不是那种类型的逻辑生成代码. However, 使用lisp进行的人工智能研究,尤其是在60年代和70年代进行的研究 MIT and Stanford创造了我们所知的这个领域,它的巨大影响还在继续.

Lisp的出现让早期的程序员看到了诸如递归之类的实用计算的可能性, 高阶函数, 第一次使用链表. 它还展示了基于lambda演算思想的编程语言的强大功能.

这些概念引发了编程语言设计的大爆发 Edsger Dijkstra正如计算机科学领域最伟大的人物之一所说, […]帮助我们许多最有天赋的人类同胞思考以前不可能的想法.”

这个例子展示了一个简单的Lisp程序(以及它在更熟悉的Python语法中的等效程序),它定义了一个函数“factorial”,该函数递归地计算其输入的阶乘,并使用输入“7”调用该函数:

LispPython
(defun factorial (n) (if (= n 1) 1 (* n (factorial (- n 1))))) (print (factorial 7))
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)

print(factorial(7))

Code as Data

尽管它是Lisp最具影响力和最重要的创新之一, homoiconicity, 不像递归和Lisp开创的许多其他概念, 今天的大多数编程语言都没有用到它吗.

下表比较了Julia和Lisp中返回代码的同形函数. 茱莉亚语是一种同形符号语言, in many ways, 类似于您可能熟悉的高级语言(e.g., Python, Ruby).

每个示例中的关键语法是its quoting character. Julia uses a : (冒号)来引用,而Lisp使用 ' (single quote):

JuliaLisp
function function_that_returns_code () return :(x + 1) end
(defun function_that_returns_code ()
    '(+ x 1))

在这两个例子中,主表达式((x + 1) or (+ x 1))将其从可直接求值的代码转换为可操作的抽象表达式. 函数返回代码,而不是字符串或数据. 如果我们调用函数,然后写 print (function_that_returns_code ()), Julia将打印字符串化的代码为 x+1 (Lisp也是如此). 相反,如果没有 : (or ' 在Lisp中),我们会得到一个错误 x was not defined.

让我们回到Julia的例子并扩展它:

函数function_that_returns_code (n)
    return :(x + $n)
end

My_code = function_that_returns_code(3)
print(my_code) #打印输出(x + 3)

x = 1
print(eval(my_code)) #输出4
x = 3
print(eval(my_code)) #输出6
代码片段6:Julia示例扩展

The eval 函数可用于运行从程序中其他地方生成的代码. 类的定义为基础输出的值 x variable. If we tried to eval 我们在上下文中生成的代码 x 没有定义,我们会得到一个错误.

同象性是一种强大的元编程, 能够解锁新颖和复杂的编程范式,程序可以在飞行中适应, 生成代码以适应领域特定的问题或遇到的新数据格式.

以WolframAlpha为例 Wolfram Language 能够生成代码以适应各种各样的问题吗. 你可以问WolframAlpha,“纽约市的GDP除以安道尔的人口是多少??,并得到了合乎逻辑的回应.

似乎没有人会想到在数据库中包含这种模糊而毫无意义的计算, 但是Wolfram使用元编程和本体知识图来编写动态代码来回答这个问题.

理解Lisp和其他同构语言提供的灵活性和强大功能非常重要. 在我们深入讨论之前,让我们考虑一下您可以使用的一些元编程选项:

 DefinitionExamplesNotes
Homoiconicity一种语言特征,其中代码是“一级”数据. 由于代码和数据之间没有分离,因此两者可以互换使用.
  • Lisp
  • Prolog
  • Julia
  • Rebol/Red
  • Wolfram Language
这里,Lisp包括Lisp家族中的其他语言,如Scheme、Racket和Clojure.
Macros将代码作为输入并返回代码作为输出的语句、函数或表达式.
  • Rust’s macro_rules!, Derive和过程宏
  • Julia’s @macro invocations
  • Lisp’s defmacro
  • C’s #define
(参见下一个关于C语言宏的注释.)
预处理器指令(或预编译器)一种以程序为输入的系统, 基于代码中包含的语句, 返回程序的更改版本作为输出.
  • C’s macros
  • C++’s # 预处理系统
C的宏是使用C的预处理器系统实现的,但这两者是不同的概念.

C语言的宏(我们在其中使用 #define 预处理器指令)和其他形式的C预处理器指令(如.g., #if and #ifndef)是我们使用宏来生成代码,而使用其他非#define 有条件地编译其他代码的预处理器指令. 这两者在C语言和其他一些语言中密切相关, 但它们是不同类型的元编程.
Reflection程序检查、修改和自省其自身代码的能力.
  • Python’s isinstance, getattr, functions
  • JavaScript’s Reflect and typeof
  • Java’s getDeclaredMethods
  • .NET’s System.Type class hierarchy
反射可以在编译时发生,也可以在运行时发生.
Generics能够编写对许多不同类型有效的代码,或者可以在多个上下文中使用但存储在一个地方的代码. 我们可以显式或隐式地定义代码有效的上下文.

泛型模板样式:

  • C++
  • Rust
  • Java

参数多态性:

  • Haskell
  • ML
泛型编程是一个比泛型元编程更广泛的主题, 两者之间的界限并不明确.

在笔者看来, 参数类型系统只有在使用静态类型语言时才算元编程.
元编程参考

让我们看一些同象性的实际例子, macros, 预处理器指令, reflection, 以及用各种编程语言编写的泛型:

#通过动态创建代码行输出“Hello Will”,“Hello Alice”
say_hi = (println("Hello, ", name))

name = "Will"
eval(say_hi)

name = "Alice"
eval(say_hi)
代码片段7:Julia中的同象性
int main() {
#ifdef _WIN32
    这一节只会在windows上编译并运行!\n");
    windows_only_function ();
#elif __unix__
    这一节只会在unix上编译和运行!\n");
    unix_only_function ();
#endif
    无论平台如何,这一行都可以运行!\n");
    return 1;
}
代码片段8:C中的预处理器指令
从pet_sdk导入猫,狗,get_宠物

pet = get_pet()

如果isinstance(宠物,猫):
    pet.clean_litterbox()
elif isinstance(宠物,狗):
    pet.walk()
else:
    print(f“不知道如何帮助类型为{type(pet)}的宠物”)
代码片段9:Python中的反射
import com.example.coordinates.*;

接口车辆{
    getName();
    public void move(double xCoord, double yCoord);
}

public class VehicleDriver {
    //这个类对任何其他类T都是有效的
    //车辆接口
    私人最终T车;

    公共车辆司机(T车辆){
        System.out.println("VehicleDriver: " + vehicle.getName());
        this.vehicle = vehicle;
    }

    public void goHome() {
        this.vehicle.移动(HOME_X HOME_Y);
    }

    public void goToStore() {
        this.vehicle.移动(STORE_X STORE_Y);
    }
    
}
代码片段10:Java中的泛型
macro_rules! print_and_return_if_true {
    ($val_to_check: ident, $val_to_return: expr) => {
        If ($val_to_check) {
            println!("Val为真,返回{}",$val_to_return);
            返回val_to_return美元;
        }
    }
}

//下面的语句与对于x, y, z的语句相同,
//我们写if x {println!...}
fn example(x: bool, y: bool, z: bool) -> i32 {
    print_and_return_if_true!(x, 1);
    print_and_return_if_true!(z, 2);
    print_and_return_if_true!(y, 3);
}
代码片段11:Rust中的宏

宏(如代码片段11中的宏)在新一代编程语言中再次流行起来. 为了成功地开发这些,我们必须考虑一个关键问题:卫生.

卫生和非卫生宏

代码的"卫生"和"不卫生"是什么意思? 的实例化的Rust宏 macro_rules! function. 顾名思义, macro_rules! 根据我们定义的规则生成代码. 在本例中,我们已经命名了宏 my_macro规则是“创建一行代码” let x = $n”, where n is our input:

macro_rules! my_macro {
    ($n) => {
        let x = $n;
    }
}

fn main() {
    let x = 5;
    my_macro!(3);
    println!("{}", x);
}
代码片段12:Rust中的卫生

当我们展开宏时(运行一个宏,用它生成的代码替换它的调用), 我们期望得到以下结果:

fn main() {
    let x = 5;
    let x = 3; // This is what my_macro!(3) expanded into
    println!("{}", x);
}
代码片段13:我们的示例,展开

看起来,我们的宏重新定义了变量 x 等于3,所以我们可以合理地期望程序输出 3. In fact, it prints 5! Surprised? In Rust, macro_rules! 在标识符方面是否卫生,因此它不会“捕获”其范围之外的标识符. 在本例中,标识符为 x. 如果它被宏捕获,它会等于3吗.

hygiene (noun)

一个属性,保证宏的扩展不会从宏的作用域之外捕获标识符或其他状态. 不提供此属性的宏和宏系统将被调用 unhygienic.

宏中的卫生在开发人员中是一个有争议的话题. 支持者坚持认为,没有卫生, 无意中微妙地修改代码的行为是非常容易的. 想象一下,有一个比Snippet 13复杂得多的宏,用于包含许多变量和其他标识符的复杂代码中. 如果该宏使用了与代码相同的变量,而您却没有注意到,该怎么办?

开发人员在没有阅读源代码的情况下使用外部库中的宏是很常见的. 这在提供宏支持的新语言中尤其常见.g., Rust and Julia):

#define EVIL_MACRO网站="http://evil.com";

int main() {
    Char *website = "http://good ..com";
    EVIL_MACRO
    send_all_my_bank_data_to(网站);
    return 1;
}
代码片段14:一个邪恶的C宏

这个不卫生的C宏捕获标识符 website 并改变它的值. 当然,标识符捕获不是恶意的. 这仅仅是使用宏的偶然结果.

因此,卫生的宏是好的,而不卫生的宏是坏的? 不幸的是,事情没那么简单. 有一个强有力的理由可以证明,卫生宏限制了我们. 有时,标识符捕获是有用的. Let’s revisit Snippet 2, where we use pet_sdk 为三种宠物提供服务. 我们最初的代码是这样开始的:

birds = pet_sdk.get_birds()
cats = pet_sdk.get_cats()
dogs = pet_sdk.get_dogs()

for cat in cats:
    # Cat特定代码
for dog in dogs:
    #狗专用代码
# etc…
片段15:回到兽医召回 pet sdk

你们应该还记得 Snippet 3 是不是试图将代码片段2的重复逻辑压缩成一个包罗万象的循环. 但是如果我们的代码依赖于标识符呢 cats and dogs,我们想写这样的东西:

{animal}s = pet_sdk.get{animal}s()
对于{animal}中的{animal}:
    #{动物}特定代码
代码片段16:有用的标识符捕获(在假想的“turbpython”中)

代码片段16有点简单, of course, 但是想象一下,如果我们想要一个宏来编写给定部分的100%的代码. 在这种情况下,卫生宏可能会受到限制.

尽管卫生与不卫生的宏观争论可能很复杂, 好消息是,这不是一个你必须表明立场的问题. 您使用的语言决定了您的宏是卫生的还是不卫生的, 所以在使用宏时要记住这一点.

Modern Macros

宏现在正处于一个小时期. For a long time, 现代命令式编程语言不再把宏作为其功能的核心部分, 避免使用它们以支持其他类型的元编程.

新程序员在学校学习的语言.g.(Python和Java)告诉他们,他们所需要的只是反射和泛型.

Over time, 随着这些现代语言的流行, 宏开始与令人生畏的C和c++预处理器语法联系在一起——如果程序员知道它们的话.

然而,随着Rust和Julia的出现,趋势又转向了宏. Rust和Julia是两个现代人, accessible, 以及广泛使用的语言,这些语言用一些新的和创新的想法重新定义和普及了宏的概念. 这对Julia来说尤其令人兴奋, 它看起来已经准备好取代Python和R作为一种易于使用的语言, “电池包括”通用语言.

当我们第一次看 pet_sdk 通过我们的“turbpython”眼镜,我们真正想要的是像Julia这样的东西. Let’s rewrite Snippet 2 在Julia中,使用它的同象性和它提供的一些其他元编程工具:

using pet_sdk

(宠物,care_fn) =((“猫”:clean_litterbox)(“狗”:walk_dog)(“狗”:clean_cage))
    get_pets_fn = Meta.parse("pet_sdk.get_${pet}s")
    @eval begin
        本地动物= $get_pets_fn() #pet_sdk.pet_sdk get_cats ().get_dogs(), etc.
        动物中的动物
            animal.$care_fn # animal.clean_litterbox(),动物.walk_dog(), etc.
        end
    end
end
片段17:Julia的宏制作功能 pet_sdk Work for Us

让我们分解代码片段17:

  1. 我们遍历三个元组. 第一个是 (“猫”:clean_litterbox), so the variable pet is assigned to "cat", and the variable care_fn 分配给引号符号 :clean_litterbox.
  2. We use the Meta.parse 函数将字符串转换为 Expression,所以我们可以把它当作代码来计算. In this case, 我们想利用字符串插值的力量, 我们可以把一个字符串放到另一个字符串里, 定义要调用的函数.
  3. We use the eval 函数来运行我们生成的代码. @eval begin… end 是另一种写作方式吗 eval(...) 以避免重复输入代码. Inside the @eval 块是我们动态生成并运行的代码.

Julia的元编程系统真正地解放了我们,使我们能够以自己想要的方式表达自己想要的东西. 我们可以使用其他几种方法,包括反射(如Python中的 Snippet 5). 我们还可以编写一个宏函数,显式地生成特定动物的代码, 或者我们可以将整个代码生成为字符串并使用 Meta.parse 或者这些方法的任意组合.

超越Julia:其他现代元编程系统

Julia可能是现代宏观系统中最有趣、最引人注目的例子之一,但事实并非如此, by any means, the only one. Rust也在将宏再次带到程序员面前方面发挥了重要作用.

在Rust中,宏的特性比在Julia中集中得多,尽管我们不会在这里详细探讨. 由于一系列原因,如果不使用宏,就不能编写习惯的Rust. 然而,在Julia中,您可以选择完全忽略同象性和宏观系统.

作为这种中心性的直接结果,Rust生态系统已经真正地拥抱了宏. 社区成员已经建立了一些非常酷的图书馆, proofs of concept, 以及使用宏的特性, 包括可以序列化和反序列化数据的工具, 自动生成SQL, 甚至可以将代码中的注释转换为另一种编程语言, 所有在编译时生成的代码.

而Julia的元编程可能更具表现力和自由, Rust可能是提升元编程的现代语言的最佳示例, 因为它在整个语言中都很重要.

放眼未来

现在是对编程语言感兴趣的绝佳时期. Today, 我可以用c++编写应用程序并在web浏览器中运行,也可以用JavaScript编写应用程序并在桌面或手机上运行. 进入门槛从未如此之低, 新程序员的信息触手可及,这是前所未有的.

在这个程序员选择和自由的世界里, 我们越来越有使用富人的特权, modern languages, 从计算机科学和早期编程语言的历史中挑选哪些特性和概念. 看到宏在这波开发浪潮中被重新拾起,这是令人兴奋的. 当Rust和Julia向新一代开发人员介绍宏时,我迫不及待地想看看他们会做些什么. 记住,“代码即数据”不仅仅是一句口头禅. 在任何在线社区或学术环境中讨论元编程时,这是一个需要牢记的核心思想.

“代码即数据”不仅仅是一句流行语.

元编程64年的历史已经成为我们今天所知道的编程发展的一部分. 虽然我们探索的创新和历史只是元编程传奇的一个角落, 它们展示了现代元编程的强大功能和实用性.

了解基本知识

  • “元编程”是什么意思?

    元编程描述了一组技术, 以及编程语言的一类特征, 程序可以检查, modify, 并通过将其视为数据来执行自己的代码.

  • 元编程的用途是什么?

    元编程技术可以用来更自然地表达复杂的算法, 减少程序员必须编写的样板代码或重复代码的数量, 通过在编译时而不是运行时运行逻辑来提高性能, and much more, 使用是特定于领域和语言的.

  • 什么是“代码即数据”?

    “代码即数据”和“数据即代码”是指许多元编程技术的修改能力的常用表达, reference, 并阅读它们所在的程序的代码, 将其作为数据处理. 这些表达式起源于Lisp,特别是在Lisp社区中非常有名.

聘请Toptal这方面的专家.
Hire Now
贝多的头像
Will Beddow

Located in 纽约,纽约,美国

Member since May 19, 2021

About the author

Will是专门设计复杂性能系统的全栈开发人员. 他最近的工作包括金融科技系统设计, 云编排程序, 以及大型企业软件系统.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

波士顿咨询集团

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.