你已經知道怎麼用一塊塊的片段拼出一份 quotation 並執行它。quotation 就只是一段程式碼序列,所以你所熟悉的那些序列相關的詞也都適用於它:
USING: kernel math prettyprint quotations sequences ;
{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9
[ 1 + ] dup append . ! => [ 1 + 1 + ]
append會接合兩份 quotation,concat會把一整個陣列的 quotation 攤平,map會把資料變成一份份 quotation,而>quotation則讓結果可以被呼叫。MACRO:正是把你這項本事搬到編譯期。
一個MACRO:定義看起來就像一個普通的詞,但它的主體是在你的程式被編譯的時候執行,而不是在程式執行時執行。主體必須在堆疊上留下一份 quotation。接著編譯器會把那份 quotation 拼接進呼叫處,也就是用主體所產生的程式碼取代巨集呼叫。到了執行期,巨集已經不存在,只剩下被產生出來的程式碼。
USING: kernel macros sequences ;
MACRO: twice ( quot -- spliced )
dup append ;
不論你在哪裡寫下[ 1 + ] twice,編譯器都會:
[ 1 + ]的狀態下執行主體,dup append會建構出 quotation [ 1 + 1 + ],1 + 1 +拼接進那個位置。所以,一個呼叫twice的詞,編譯出來的結果就像你親手寫下1 + 1 +一樣:
: inc-twice ( x -- y ) [ 1 + ] twice ;
5 inc-twice . ! => 7
最關鍵的對比是:一般的詞如果接受一份 quotation,會在執行期用call呼叫它。巨集則從不呼叫任何東西,它是把新的程式碼交給編譯器,讓編譯器在原地編譯這些程式碼。
因為主體是在編譯期執行,巨集的輸入必須在當時就已經知道。它們在呼叫處必須是字面值,例如一份[ … ] quotation、一個數字、一個陣列、一個字串。只存在於執行期的值沒辦法交給巨集。
! DOCTEST: SKIP — illustrative call sites, not standalone code
[ 1 + ] twice ! ok: the quotation is a literal
[ 1 + ] 3 repeat-quot ! ok: the quotation and the number are literals
巨集主體就是一段會回傳 quotation 的普通程式碼。大部分的工作,你已經見過的詞就能勝任:
| 想建構出… | 使用 |
|---|---|
從[ A ]和[ B ]造出[ A B ]
|
append |
| 從一整份陣列造出單一份 quotation | concat >quotation |
| 為序列的每個元素各造一份 quotation |
map,然後concat >quotation
|
還有兩個詞來自sequences和kernel,湊齊了整套工具。
<repetition>(位於sequences)會把單一元素複製成n份,做出一段序列。它的堆疊效應是( length elt -- repetition ),其中數量會排在最前面。結果是一段虛擬序列,所以照平常的方式把它攤平:
3 [ 1 + ] <repetition> concat >quotation . ! => [ 1 + 1 + 1 + ]
curry(位於kernel)會把一個值加到 quotation 的最前面,產生一份會先推入那個值的新 quotation:
3 [ + ] curry . ! => [ 3 + ]
{ 1 2 3 } [ . ] curry . ! => [ { 1 2 3 } . ]
curry就是讓你把某個特定元素嵌進 quotation 的方法,也正是把{ 10 20 30 }和[ + ]變成[ 10 + ] [ 20 + ] [ 30 + ]的關鍵。
在某些語言裡,巨集需要特別的 quote/unquote 語法,還需要額外的機制來避免名稱衝突。Factor 完全不需要這些:一份 quotation 本身就是一串已經解析完成的詞,剖析器在讀到[ … ]的時候,就已經查好每個dup、+和append。所以巨集主體不過就是你平常就在做的序列操作,只是它剛好會回傳一份 quotation。
$[ … ]
巨集回傳的 quotation 會被拼接進一次呼叫裡。如果你想要的是在剖析期就算好的單純值,$[ code ]會在檔案被讀取的同時執行code,並把結果以字面值的形式折入原始碼中:
! DOCTEST: SKIP — compile-time value, shown for contrast
USING: calendar literals ;
CONSTANT: build-time $[ now ]
$[ … ]出自literals詞彙,它收集了用來把剖析期結果內插到字面值裡的詞。這裡提到它只是為了完整。接下來你要寫的四個模板:twice、repeat-quot、compose-many和each-literal,全都是MACRO:宣告。
你正在維護一座訊號站的程式碼產生工具組。 每個巨集都是一個模板,也就是一個小範本,編譯器會用它來在每個呼叫端印製出特製化的程式碼。
你只需要寫這 4 個巨集。測試檔定義了一些小小的包裝詞,用來呼叫你的巨集(巨集只能透過它產生出來的程式碼來觀察),接著檢查這些包裝詞的執行結果。包裝詞請保持原樣。
每個任務中,巨集主體會從堆疊上收到呼叫端的字面輸入,並且必須留下一個引號。「編譯成」那幾行顯示你的引號應該產生出來的直線式程式碼。
twice接受一個字面引號,並回傳一個會把原本的引號連續執行兩次的引號。
! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ] -> must build [ 1 + 1 + ]
5 inc-twice . ! => 7
repeat-quot接受一個字面引號和一個字面次數n,並回傳一個會把原本的引號連續執行n次的引號。n = 0會回傳空引號[ ]。
! : inc-thrice ( x -- y ) [ 1 + ] 3 repeat-quot ;
! body receives [ 1 + ] 3 -> must build [ 1 + 1 + 1 + ]
5 inc-thrice . ! => 8
compose-many接受一個由引號組成的字面陣列,並回傳一個會依序執行每一個引號的單一引號。空陣列會回傳空引號。
! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] } -> must build [ 1 + 2 * 1 - ]
5 pipeline . ! => 11
each-literal接受一個字面序列和一個引號。它會回傳一個引號,這個引號會針對序列中的每個元素,推入該元素,然後執行那個引號。這是發生在編譯期的疊代,所以產生出來的程式碼是直線式的。
這是四個之中結構性最強的一個:序列的長度和它的元素值都會形塑產生出來的程式碼。提示會帶你一步一步把它建構出來。
! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ] -> must build [ 10 + 20 + 30 + ]
sum-three . ! => 60