跳轉到內容

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) 語句不會更改宏擴充套件的值。

示例用途

[編輯 | 編輯原始碼]

超程式設計

[編輯 | 編輯原始碼]

可變長度引數列表。

[編輯 | 編輯原始碼]
華夏公益教科書