跳转到内容

Lush/宏

来自维基教科书,开放世界中的开放书籍

一个是一个元函数,它返回一些要运行的代码。对于宏的用户来说,宏看起来和行为都与函数完全一样。事实上,大多数 Lush 用户可以在不关心哪些函数实际上是宏的情况下,熟练使用它的内置库。

为什么要写宏?

[编辑 | 编辑源代码]

宏比函数更繁琐,更难调试。然而,它们在某些情况下提供的优势使它们物有所值

  • 编译代码中的参数类型灵活性 通常人们想要编写一个可编译的函数,它可以接受任何数值类型或 idx 作为参数。在编译的 lush 中,你需要限制每个函数只能接受一种特定类型。宏函数不必那样限制自己,只要生成的代码能够使用给定的参数类型进行编译。
  • 元编程:有时我们会编写许多函数,这些函数的代码体几乎相同,除了几个孤立的差异。一个宏可以根据其参数的读取时特征扩展到这些不同的函数。
  • 可变长度参数列表:宏可以具有可变长度的参数列表。这可以用作可编译的替代方案,用于使用 (optional&) 参数的常规函数,这些参数不可编译。

编译宏的局限性

[编辑 | 编辑源代码]

在编译包含宏的表达式时,宏会被剥夺了许多在运行时才可用的信息。这限制了编译宏使用任何类型的运行时信息,例如

  • 参数类型 类型信息,例如 idx 的数值类型,无法在编译时从宏的参数中推断出来(宏参数通常将具有通用符号类型 |SYMBOL|)。
  • 张量维数 与其他一些编程语言不同,Lush 张量具有不同的维数(例如向量与矩阵),但都属于同一类型 idx。因此,让宏根据 idx 维数进行编译时决策将注定失败。

那么,在可编译的宏中,你可以切换什么?任何在编译时已知的价值,例如

  • 字面值 你可以使宏根据 int 参数进行编译时决策,只要该参数作为字面值提供,而不是作为计算表达式的结果。
  • 参数数量 请参见以下关于如何实现可变长度参数列表的内容。

如何编写宏

[编辑 | 编辑源代码]

宏是在读取时进行评估的函数,并且返回函数代码体。考虑这个简单的非宏函数

 ; Function that adds two numbers
 (de add-nums (num-1 num-2)
   (+ num-1 num-2))

宏等效项可以这样编写

 ; Macro that adds two numbers '''(naive implementation)'''
 (dm add-nums-macro (fname num-1 num-2)
   (list '+ num-1 num-2))

如你所见,宏函数使用 dm 而不是 de 声明。此外,它们还有第一个额外的参数 fname,它包含宏的名称。最后,它们不计算函数的答案,而是计算一个表达式,该表达式在评估时返回函数代码体。在上面的宏中,(list '+ num-1 num-2) 将在读取时评估为 (+ value_of_num-1 value_of_num-2)


以下内容将向你展示将函数机械地转换为宏所需的基本更改。(有人应该编写一个执行此操作的宏!)

用列表生成器替换列表

[编辑 | 编辑源代码]

任何 lush 表达式都是圆括号中的元素列表

(a b c d)

在函数的宏版本中,所有这样的列表都应更改为列表生成器

(list a b c d)

此规则应递归地应用于每个列表元素。如果元素是列表,则将其替换为列表生成器。否则,用一个前导 ' 对其引用,以便它按原样返回,而不进行评估。

; normal code
(sum (range 5))

; macro code
(list 'sum (list 'range 5))

引用所有非参数符号

[编辑 | 编辑源代码]

请注意,在上面的示例中,sumrange 前面有一个撇号。这可以防止解释器在读取时评估符号“sum”的值并将其更改为“::DX:sum”。同样,符号,如运算符(“+”)、类名(“gb-module”)和非参数变量都需要用撇号“引用”,以便它们按原样进入生成的函数代码体。

以下是一些引用示例

; normal code:
(+ 1 2 3)

; macro code (don't need to quote numeric literals):
('+ 1 2 3)

; normal code (embedded C code):
#{ exit(0); #}

; macro code:
'#{ exit(0); #}

; normal code
(==> my-object some-method)

; macro code
(list '==> 'my-object 'some-method)

; normal code
(:my-object:some-slot)

; macro code (expands the ':' shorthand into the 'scope' function that it represents).
(list 'scope 'my-object 'some-slot)

请注意,字面量,如上面的数字字面量 123,或字符串字面量,如 "I'm a string",不需要引用。

将多行函数放入一个封闭列表中

[编辑 | 编辑源代码]

(写关于使用 list、progn 或 let 的内容)。

最多使用一次参数变量

[编辑 | 编辑源代码]

上面的 add-nums-macro 示例是一个幼稚的实现,因为它直接使用了参数。这样做的问题是,每次在宏中使用 num-1num-2 时,都会进行评估。例如,如果用户将 (incr x) 插入 num-1 位置,则该函数将在宏中每次出现 num-1 时都被调用。对于 add-nums-macro 这样的简单宏来说,这很好,因为每个参数只使用一次,但大多数函数会多次引用其参数。一个简单的解决方法是在宏的开头将参数分配给局部变量

 (dm idx-scale-and-add (idx-a-arg idx-b-arg)
   (list 'let* (list (list 'idx-a idx-a-arg) 
                     (list 'idx-b idx-b-arg))
         ;; scale idx-a by 5
         (list 'idx-dotm0 'idx-a [d@ 5] 'idx-a)
         ;; add the scaled idx-a to idx-b
         (list 'idx-add 'idx-a 'idx-b)))

养成用 let* 包含你的宏代码体的习惯,还有助于确保所有宏行都被返回,而不仅仅是最后一行(参见上一节)。

调试宏

[编辑 | 编辑源代码]

在你可以用 Lush 编写的三种代码中(解释型、编译型和宏),宏代码可以说是最难调试的,因为错误可能出现在读取时代码生成中,也可能出现在生成的运行时代码中。在调试运行时代码时,你可以使用与调试普通函数时相同的函数,只是像前面描述的那样“宏化”。换句话说,(pause) 变成 (list 'pause)(printf) 变成 (list 'printf),等等。在调试读取时生成时,你可以使用以下工具

(macro-expand)

[编辑 | 编辑源代码]

编写宏后,你应该做的第一件事是查看它生成的代码。使用 (pretty (macro-expand <macro expression>)),它会格式化并打印出宏表达式生成的代码。

示例

 ;; Expanding the expression "(select (double-matrix 2 2) 0 1))"
 ? (pretty (macro-expand (select (double-matrix 2 2) 0 1)))
 (let ((_-m (idx-clone (double-matrix 2 2))))
   (idx-select _-m 0 1)
   _-m )

你也可以插入 (print) 语句,以打印出某些表达式的编译时值,只要你小心确保这些 (print) 语句不会更改宏扩展的值。

示例用途

[编辑 | 编辑源代码]

元编程

[编辑 | 编辑源代码]

可变长度参数列表。

[编辑 | 编辑源代码]
华夏公益教科书