學習軌道
/
Factor
Factor
/
練習
/
信號模板
信號模板

信號模板

學習練習

簡介

你已經知道怎麼用一塊塊的片段拼出一份 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. 在堆疊上放著[ 1 + ]的狀態下執行主體,
  2. dup append會建構出 quotation [ 1 + 1 + ],
  3. 把其中的程式碼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 個巨集。測試檔定義了一些小小的包裝詞,用來呼叫你的巨集(巨集只能透過它產生出來的程式碼來觀察),接著檢查這些包裝詞的執行結果。包裝詞請保持原樣。

每個任務中,巨集主體會從堆疊上收到呼叫端的字面輸入,並且必須留下一個引號。「編譯成」那幾行顯示你的引號應該產生出來的直線式程式碼。

1. 執行兩次

twice接受一個字面引號,並回傳一個會把原本的引號連續執行兩次的引號。

! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ]  ->  must build [ 1 + 1 + ]
5 inc-twice .   ! => 7

2. 重複 N 次

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

3. 組合多個

compose-many接受一個由引號組成的字面陣列,並回傳一個會依序執行每一個引號的單一引號。空陣列會回傳空引號。

! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] }  ->  must build [ 1 + 2 * 1 - ]
5 pipeline .   ! => 11

4. 展開的疊代

each-literal接受一個字面序列和一個引號。它會回傳一個引號,這個引號會針對序列中的每個元素,推入該元素,然後執行那個引號。這是發生在編譯期的疊代,所以產生出來的程式碼是直線式的。

這是四個之中結構性最強的一個:序列的長度和它的元素值都會形塑產生出來的程式碼。提示會帶你一步一步把它建構出來。

! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ]  ->  must build [ 10 + 20 + 30 + ]
sum-three .   ! => 60
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Factor Exercism

準備好開始 信號模板 了嗎?

註冊 Exercism,透過 47 個概念163 個練習 和真人引導來學習並精通 Factor,全部免費。