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))
請注意,在上面的示例中,sum 和 range 前面有一個撇號。這可以防止直譯器在讀取時評估符號“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)
請注意,字面量,如上面的數字字面量 1、2 和 3,或字串字面量,如 "I'm a string",不需要引用。
(寫關於使用 list、progn 或 let 的內容)。
上面的 add-nums-macro 示例是一個幼稚的實現,因為它直接使用了引數。這樣做的問題是,每次在宏中使用 num-1 或 num-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),等等。在除錯讀取時生成時,你可以使用以下工具
編寫宏後,你應該做的第一件事是檢視它生成的程式碼。使用 (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) 語句不會更改宏擴充套件的值。