Intro
最近寫代碼感覺非常的不順。 之前看自己一個月以來寫的代碼真的是被自己惡心到了吭狡。 感覺我的Racket 還是沒有能夠掌握主要特性。 所以大概就花了一周左右的時間把Macro拿出來學(xué)了一下丈莺, 今年的PL課上很可惜沒有時間在課上講這個划煮。
如果說編程語言是魔咒學(xué)的話, 這個Macro 絕對是次元魔法缔俄, 特別是在Racket語言當(dāng)中弛秋。 簡單的說, Macro就是能讓你在正式編譯之前的,先對你的代碼進(jìn)行一次預(yù)編譯俐载, 在這個時候把你代碼中的一些符合條件的規(guī)則蟹略,全部替換成你需要的代碼,或者說去生成你想要的代碼遏佣。 之前寫過C語言的宏的話應(yīng)該還是對這個東西不陌生的挖炬,但是在大部分的語言中,宏只能做一些非常Ad-hoc的替換状婶,如果你的替換方式非常的復(fù)雜茅茂, 這時候就會很難寫,也很容易出Bug太抓。但是在Racket之類的Scheme系語言當(dāng)中,宏系統(tǒng)基本上就是能讓你直接在編譯期運(yùn)行你寫的scheme/racket代碼令杈。得益于這種強(qiáng)大的能力你可以輕松進(jìn)入高次元走敌,在那里直接對racket這個編程語言本身進(jìn)行擴(kuò)展得到你想要的語法特性。Racket 是 Racket的meta language
交換
在編程中我們經(jīng)常會需要去交換兩個變量的值逗噩。這個在命令式語言中非常的簡單掉丽, 可以直接用/
temp = a;
a = b;
b = tmp;
在racket當(dāng)中我們也不難能夠?qū)懗鱿旅娴腸ode
(let ([tmp x])
(set! x y)
(set! y tmp))
雖然racket作為函數(shù)式語言這種命令式編程語言中的“變量”,其實(shí)在racket中非常的少用异雁。
寫過匯編的都知道其實(shí)匯編里面描述交換的時候可以直接使用XCHG
捶障。這個語義其實(shí)非常的好, 我們希望在我們的編程語言中能夠?qū)崿F(xiàn)這一種語法結(jié)構(gòu)纲刀。根據(jù)metacircular interpretor 的思想,如果我們把racket當(dāng)作racket本身的meta language, 我們需要做的就是把xchg
這個語法“翻譯”到上面的這這段racket code项炼。
這種替換非常的直接, 我們可以用define-syntax-rule
來直接在我們的代碼中給racket添加一條語法規(guī)則
(define-syntax-rule (xchg x y)
(let ([tmp x])
(set! x y)
(set! y tmp)))
當(dāng)然你如果把上面的define-syntax-rule
改成define
代碼的運(yùn)行結(jié)果不會發(fā)生任何的改變示绊。
那和函數(shù)有什么區(qū)別锭部?
其實(shí)這里編譯器就相當(dāng)于在編譯值之前先把你所有代碼中的xchg 修改成了(let ...)你的代碼本身發(fā)生的變化,而如果定義函數(shù)的方式來實(shí)現(xiàn)面褐,這里實(shí)際上就是一個函數(shù)調(diào)用拌禾,會新建一個函數(shù)棧在運(yùn)行時,但是你的代碼一旦寫好了展哭,自然是不會被編譯器修改的湃窍。
這里很明顯的感覺就是使用s 表達(dá)式的優(yōu)勢, 因?yàn)槲覀兊拇a相當(dāng)于已經(jīng)天然的被parse好了闻蛀,所以很容易看清語法的邊界, 這種代碼和數(shù)據(jù)(list)的同構(gòu)性您市,讓寫宏的人會更加清晰觉痛。
使用racket編寫語法謂詞
如果我們要使用racket來寫一個簡單的解釋器,第一步需要做的就是定義目標(biāo)語言的predicate, 這樣我們就可以配合quasi quoting和pattern match來比較輕松的定義語言的語法了,比如簡單的lambda的語法如下
lambda-exp :: x
| (lambda-exp lambda-exp)
| (lamdba (x ...) lambda-exp)
where x is symbol
使用racket predicate來描述
(define (lambda-exp? e)
(match e
[(? symbol?) #t]
[`(lambda (,x ...) ,(? lambda-exp?)) #t]
[`(,(? lambda-exp? e1) ,(? lambda-exp?)) #t]))
不過感覺這一學(xué)期下來給我的感受就是墨坚,其實(shí)大部分人對于quasi quoting pattern match的閱讀能力幾乎是0........很多人到了期末都沒徹底搞明白秧饮。
這個其實(shí)是一種在racket當(dāng)中比較常見的做法但是確實(shí)會帶來一些的閱讀困難,同時這種語法結(jié)構(gòu)其實(shí)并不是那么容易去擴(kuò)展的泽篮, 在簡單單一的一組EBNF規(guī)則中其實(shí)是看不太出這個盗尸。但是如果我希望做到類似與extend一組規(guī)則的時候這種結(jié)構(gòu)其實(shí)就會顯的比較亂了。 如果我們是要寫一個編譯器帽撑,在常見的教程和方法中都會要求有非常多級的IR泼各,特別是如果我們使用nano-pass style, 就需要非常對級不同的IR亏拉,每級只是對中間的某一個小部分進(jìn)行修改扣蜻,這時候純使用predicate就會有點(diǎn)難受了。
這時候我們可以使用宏及塘,在racket中定義一個語法結(jié)構(gòu)叫define-ir
, 我們可以編寫類似EBNF的代碼莽使,然后macro expand之后我們就可以自動得到我們想要的predicate
使用宏生成語法謂詞
1. syntax
這個其實(shí)還是比較復(fù)雜的,可以一點(diǎn)一點(diǎn)來笙僚, 先看一種比較簡單的情況:
我們的宏會take 一個list,最終在編譯之后把list上的所有終結(jié)符(terminate symbol) 全部替換成quasi pattern,也就是說比如
'(symbol symbol)
在 expand 之后會變成 `(,(? symbol?) ,(? symbol?))
對于宏我們的本質(zhì)就是把代碼結(jié)構(gòu)就像list一樣進(jìn)行演算芳肌。盡管我們的s表達(dá)式非常的像list,但是很明顯'(symbol symbol)
只是一個list data, 如何在racket表示一段代碼'(symbol symbol)
呢肋层?
其實(shí)和quote有點(diǎn)像我們可以使用syntax quote,#'關(guān)鍵字
#'(symbol symbol)
這樣表示的就是一段syntax對象而不是一個list對象了亿笤。
我們甚至還可以使用quasi syntax, 這樣我們就可以在一個syntax object中間使用#,來混入racket代碼了
(define x #'c)
#`(a b #,x)
我們也可以用racket的內(nèi)置函數(shù)synatx->datum
\ datum->syntax
來完成這種data 和syntax的互相轉(zhuǎn)化
> (require racket/syntax)
> (syntax->datum #'symbol?)
'symbol?
> (syntax->datum #'(a b c))
'(a b c)
> (datum->syntax (current-syntax-context) 'x)
#<syntax x>