在上一篇《編程語言的一些基礎(chǔ)概念(一)》中,通過靜態(tài)類型的函數(shù)式編程語言兄纺,介紹了一些編程語言的特性大溜,包括數(shù)據(jù)不可變,尾遞歸估脆,匿名函數(shù)等钦奋。這一篇在上篇的基礎(chǔ)上,通過 Dynamic Typing (動態(tài)類型) 的函數(shù)式編程語言 Racket疙赠,再介紹一些編程語言的特性付材,比如 Stream, 惰性求值, 宏 Macro 等。
括號的使用
天花亂墜的括號圃阳,這是 Racket 和 LISP 等這類語言最直觀的特征厌衔。看一些例子:
(+ 1 2)
(and true (= 1 2))
這種代碼寫的方式和平時(shí)寫的挺不一樣的捍岳,(+ 1 2)
表示 1 + 2
富寿,(and true (= 1 2))
表示 true and (1 = 2)
的 boolean。大概也能看出個規(guī)律锣夹,( 后面第一個詞页徐,是要做的操作
,多數(shù)情況下是函數(shù)調(diào)用银萍,之后接的類似于函數(shù)調(diào)用要穿進(jìn)去的參數(shù)变勇。
再舉一個例子:
(define (fact n) (if (= n 0) 1 (* n (fact (- n 1)))))
上面這段代碼是典型的斐波那契函數(shù)的遞歸寫法。
在寫了一些 Racket 的程序后贴唇,最直接的感覺就是修改代碼麻煩贰锁,不容易 Debug。這種寫法對于寫代碼前邏輯的清晰度要求更高滤蝠,因?yàn)槔ㄌ枖?shù)量多,常呈卩郑看的眼花繚亂物咳,如果再加上邏輯不清晰的話,修改起來不是一般的困難蹄皱。從 syntax 的角度來說览闰,并不是很友好芯肤,為什么語言的設(shè)計(jì)要將括號的地位放到這么高的地位呢?
將所有的表達(dá)式都加上括號压鉴,最大的好處是讓編程語言的表意很明確崖咨,比如說 1 + 2 * 3
,在沒有括號情況下油吭,我需要去判斷乘法的優(yōu)先級比加法高击蹲,然后計(jì)算。在有括號的情況下婉宰, (+ 1 (* 2 3))
或者 (* (+ 1 2) 3)
歌豺,可以很明顯的知道該怎么去運(yùn)算,按照括號的順序執(zhí)行就行了心包。
第二個好處就是类咧,這種表達(dá)式很容易被分解成樹
的結(jié)構(gòu),程序的執(zhí)行順序從樹的根往下蟹腾,簡單明了痕惋。比如說下面這個例子:
像我們這種寫慣了順序表達(dá)的,對這種有一點(diǎn)點(diǎn)逆序表達(dá)的是有一些偏見的娃殖,但是不能因?yàn)槟敲炊嗬ㄌ柕氖褂弥荡粒シ穸ㄒ婚T編程語言,去否認(rèn)一門編程語言優(yōu)秀的地方珊随。
變量作用域
變量的作用域在任何一門編程語言里述寡,都是非常重要的一部分,因?yàn)樗苯記Q定了變量的值叶洞。ML 是 Lexical Scope鲫凶。Racket 有4種不同定義變量的方式,每一種變量的作用域都不太一樣 衩辟,他們分別是:let, let*, letrec, define
let
Let 綁定變量螟炫,變量值從 Let 這個定義之前來。
(define (silly-double x)
(let ([x (+ x 3)]
[y (+ x 2)])
(+ x y -5)))
[x (+ x 3)]
第二個 x 的值是函數(shù)傳入的值艺晴, [y (+ x 2)]
中 x 的值不是 [x (+ x 3)]
中綁定的昼钻,而是函數(shù)傳入的 x 的值。
let*
Let* 綁定變量封寞,變量的值從之前的綁定來然评。這和 ML 的 let 是類似的。
(define (silly-double x)
(let* ([x (+ x 3)]
[y (+ x 2)])
(+ x y -8)))
[x (+ x 3)]
第二個 x 的值仍然是函數(shù)傳入的值狈究,但是 [y (+ x 2)]
中 x 的值不再是函數(shù)傳入的值碗淌,而是 [x (+ x 3)]
中綁定的。
letrec
Letrec 綁定變量,變量的值是在包含所有綁定的環(huán)境下求得的亿眠。通常用在遞歸的情況下碎罚。
(define (silly-mod2 x)
(letrec
([even? (lambda(x)(if (zero? x) #t (odd? (- x 1))))]
[odd? (lambda(x)(if (zero? x) #f (even? (- x 1))))])
(if (even? x) 0 1)))
even 中用了 odd,odd 中用了 even纳像,互遞歸荆烈,用 let, let* 都會有問題,只能用 letrec竟趾。
define
(define (f x) (+ x 1))
define 是最普遍的變量綁定方式憔购,通常用在函數(shù)一開始的定義上。
延遲求值 Delayed Evaluation
延遲求值的作用是減少不必要的運(yùn)算潭兽。
用來延遲求值的空參的函數(shù)倦始,在計(jì)算機(jī)領(lǐng)域成為 Thunk
,可以說 Thunk the expression山卦。
; 正常表達(dá)式
e
; 延遲求值
(lambda () e)
; 求值
(e)
在 Racket 中鞋邑,函數(shù)的具體內(nèi)容只有在被調(diào)用時(shí)才會求值,我們將一個表達(dá)式包在一個函數(shù)里账蓉,只有在這個函數(shù)被調(diào)用時(shí)枚碗,這個表達(dá)式才會求值,(lambda () e)
像這樣铸本,用 lambda
構(gòu)造一個空參函數(shù)肮雨,來延遲對表達(dá)式 e
的求值。
為什么說延遲求值可以減少運(yùn)算呢箱玷?
define (f thunk)
(if (…)
0
(… (thunk) …)))
在這個例子中怨规,th 是一個 thunk,在 if 條件是 true 的情況下锡足,th 不會被求值波丰,只有在 if 條件是 false 的情況下,才會求值舶得,這樣可以減少不必要的運(yùn)算掰烟。
惰性求值 Lazy Evaluation
像上面那樣延遲求值,雖然是很直觀的可以減少運(yùn)算沐批,但沒用好纫骑,反而會增加運(yùn)算量。
(define (f thunk)
(if (…) 0 (… (thunk) …))
(if (…) 0 (… (thunk) …))))
在這個例子中九孩,有多個 if 條件的情況下先馆,每次 false,都要重復(fù)對 thunk 求值躺彬,有 n 個這種判斷磨隘,就要增加 n 倍的運(yùn)算缤底。
既然問題是多次重復(fù)運(yùn)算,那能不能將第一次運(yùn)算的值先存起來番捂,之后需要直接用,不用再運(yùn)算了江解。這其實(shí)就是惰性求值设预,延遲求值,記錄第一次求值結(jié)果犁河,之后不用再運(yùn)算一次鳖枕。
(define (my-delay thunk)
(mcons #f thunk))
(define (my-force p)
(if (mcar p)
(mcdr p)
(begin (set-mcar! p #t)
(set-mcdr! p ((mcdr p)))
(mcdr p))))
; 用法
; 函數(shù) f 里 th 部分得修改成 (my-force th)
(f (my-delay (lambda () e)))
; 函數(shù) f 不需要修改,調(diào)用時(shí)復(fù)雜
(f (let [p (my-delay (lambda () e))]
(lambda () (my-force p))))
代碼不是太好懂桨螺,mcons
表示構(gòu)建一個可以修改的 list (#f, thunk)
宾符,通過 set-mcar!
,set-mcdr!
可以修改 list 的第一個和第二個值灭翔。 my-force
是對延遲求值的調(diào)用魏烫,每一次調(diào)用,都會看看 (mcar p)
這個值是不是 True肝箱,如果是的話哄褒,說明已經(jīng)求值過啦,直接用 (mcdr p)
取值煌张;如果不是的話呐赡,(set-mcar! p #t)
把 (#f thunk)
這個的 #f 改成 True,然后求值 ((mcdr p))
骏融,(set-mcdr! p ((mcdr p))
把 (#f thunk) 的 thunk 替換成求值的結(jié)果链嘀。
流 Stream
Stream 是一個可以無限"取數(shù)據(jù)"的流,每次一次都可以拿到這個流的下一個數(shù)據(jù)档玻。比如說一個自然數(shù)的流怀泊,1 2 3 4 5 6 …,第一次是1窃肠,下一次調(diào)用包个,就是2,以此類推冤留。
這個概念在計(jì)算機(jī)領(lǐng)域還是挺普遍的碧囊,比如說 socket, message queue 等。但是作為一門編程語言的一個"特性"纤怒,還是第一次見到糯而。
; 定義流 1 1 1 1 1 1 ...
(define ones (lambda () (cons 1 ones)))
; 定義流 1 2 3 4 5 6 ...
(define nats
(letrec ([f (lambda (x) (cons x (lambda () (f (+ x 1)))))])
(lambda () (f 1))))
上面定義了兩個 stream。用的是 遞歸 + 延遲求值泊窘,非常的巧妙熄驼!
; 使用
(car (s))
(car ((cdr (s))))
(define (number-until stream tester)
(letrec ([f (lambda (stream ans)
(let ([pr (stream)])
(if (tester (car pr))
ans
(f (cdr pr) (+ ans 1)))))])
(f stream 1)))
(number-until nats (lambda (x) (= x 10))
(car (s))
可以取流里的一個數(shù)據(jù)像寒,通常應(yīng)該放在遞歸里,不斷的去調(diào)用得到下一個流里的數(shù)據(jù)瓜贾。
宏 Macro
Macro 全稱是 macroinstruction诺祸,指的是把較長的指令序列用某種規(guī)則對應(yīng)到較短的指令序列的規(guī)則或模式。在編程語言中祭芦,可以自己去定義一些宏筷笨,意味著可以在編程語言的基礎(chǔ)上去做語法的擴(kuò)展,或者替換龟劲。比如說 Racket 中的條件判斷是 (if e1 e2 e3)胃夏,如果 e1 是 true,執(zhí)行 e2昌跌,否則執(zhí)行 e3仰禀。有了宏,你可以定義自己的條件判斷 (my-if e1 then e2 else e3)
的語句蚕愤。
(define-syntax my-if ; macro name
(syntax-rules (then else) ; other keywords
[(my-if e1 then e2 else e3) ; macro use
(if e1 e2 e3)])) ; implementaion
Macro 給了程序員很大的自由度答恶,被放飛了就很容易被濫用/過度使用,有一些函數(shù)能解決的問題审胸,偏偏還要定義成 macro亥宿,所以可以有一個不成文的準(zhǔn)則 當(dāng)你不知道該用 function 還是 macro 時(shí),用 function 就對了砂沛。
比 C/C++ 的 Macro 更"強(qiáng)大"
Racket 的 Macro 的編譯解析比 C/C++ 的更加完善烫扼。舉一個例子:
(define-syntax double
(syntax-rules ()
[(double x) (* 2 x)]))
(let ([* +]) (double 42))
; 展開
(let ([* +]) (* 2 42))
使用 macro double,在展開后是 (let ([* +]) (* 2 42))
碍庵,let 將 * 綁定成 +映企,(* 2 42)
這里的運(yùn)算,應(yīng)該是 + 還是 *静浴?在 Racket 中堰氓,macro 里定義的(* 2 42)
仍然是乘法運(yùn)算,但是 C/C++ 則會因?yàn)榻壎ㄆ幌恚幊碳臃ㄟ\(yùn)算双絮,導(dǎo)致了 macro 和使用 macro 的變量需要很小心的定義,不然一不小心就會出現(xiàn)奇怪的很難發(fā)現(xiàn)的 bug得问。
判斷條件
if condition else ...
在靜態(tài)語言中囤攀,condition 做為判斷條件,必須是 boolean 類型宫纬,只能是 true 或者 false焚挠。但是在像 Racket 這類的動態(tài)語言中, 條件定義好的會是 False漓骚,其他表達(dá)式都判斷為 True蝌衔。在 Racket 里榛泛,只有 #f,但是在 Python 中噩斟,False
, 0
, []
等都是會判斷為 False曹锨。
對于靜態(tài)語言來說,類型必須正確剃允,所以判斷條件一定必須要是 Boolean艘希,不然 Type Inference, Type Check 等很難成立。對于動態(tài)語言來說硅急,就自由很多,Racket佳遂,Python 以及其他的動態(tài)語言會有各自不同的定義吧营袜。這影響的是編程語言的語法,和一定的代碼風(fēng)格丑罪,是一個挺有趣的小點(diǎn)荚板。
總結(jié)
這些內(nèi)容是 Coursera 上 Programming Languages, Part B 內(nèi)容的總結(jié)和筆記。知道與不知道這些吩屹,對平時(shí)工作沒有太大的影響跪另,但是可以拓寬對于編程語言的認(rèn)識。你只能想到你知道的煤搜,在接觸一門新的編程語言時(shí)免绿,可以想想這些特性在那個編程語言上是怎么實(shí)現(xiàn)的,作者為什么這么去實(shí)現(xiàn)擦盾,從這個角度嘲驾,或許能更深的認(rèn)識這個語言。