Clojure
零基礎(chǔ)
學(xué)習(xí)筆記
歡迎來到 Clojure 的世界宰睡。
讓我們先從最經(jīng)典的 hello world 開始吧唾戚。
我們使用鍵盤在 REPL 的輸入框里輸入 (print "hello world!")
,回車咳秉!
屏幕中就會顯示:
=> (print "hello world!")
hello world!
nil
(如果你還不知道怎么啟動 REPL爽蝴,你可以看一下這篇文章:“最小化”運行 Clojure REPL )
首先說明本文的幾個約定
-
=>
后面跟隨的內(nèi)容,表示在 REPL[1] 里輸入的內(nèi)容 - 在
=>
之后另起新行出現(xiàn)的代碼昧辽,為 REPL 返回 (即 REPL 中的 Print) 的內(nèi)容 - 在
;=
之后出現(xiàn)的內(nèi)容,表示上一個表達(dá)式的值
如你所見登颓,Clojure 所擁有的 REPL 環(huán)境可以快速地與你進(jìn)行交互 --- 表達(dá)式[2]被立即執(zhí)行搅荞。
現(xiàn)在讓我們看看這三行代碼分別表示什么吧
- 第 1 行是我們在 REPL 中輸入的內(nèi)容,比如使用鍵盤輸入框咙。這段代碼的含義是咕痛,告訴 REPL ,我們要執(zhí)行
print
函數(shù)喇嘱,以及提供函數(shù)所需的參數(shù) - 第 2 行的
hello world!
為print
函數(shù)的副作用[3]茉贡。(而我們所需要的正是這個副作用) - 第 3 行的
nil
為print
函數(shù)的返回值[4],print
函數(shù)始終返回nil
者铜。(nil
在 Clojure 中表示空值)
所以我們可以大概知道 REPL 是怎么運行的:
首先腔丧,他接受你的輸入。
然后王暗,執(zhí)行你所輸入的代碼悔据,如果有副作用就會觸發(fā)副作用庄敛。
最后俗壹,它返回你所輸入的代碼的值。REPL 總是把你所輸入的表達(dá)式的值在最后一行顯示出來
函數(shù)藻烤,是 Clojure 里最為重要也是最為基本的組成部分绷雏。
就如同你在中學(xué)數(shù)學(xué)學(xué)習(xí)到的 f(x,y)
一樣怖亭,函數(shù)一般由三部分組成:
- 函數(shù)的名稱涎显。
如print
。在 Clojure 中兴猩,它寫在在小括號()
中的第一個位置期吓。 - 函數(shù)的參數(shù)。
如果函數(shù)可以接受多個參數(shù)倾芝,多個參數(shù)之間用空格隔開讨勤。(也可以用,
) - 函數(shù)的返回值。
也就是函數(shù)的值晨另。
所以 f(x潭千,y)
在 Clojure里就表示為 (f x y)
此例中的 print
函數(shù)
它接收任意數(shù)量的參數(shù),
它的返回值永遠(yuǎn)是 nil
借尿,也就是空刨晴,空值屉来。
而 print
函數(shù)除了返回值之外,還擁有一個“副作用”狈癞,那就是它會依次把每個參數(shù)的值顯示在屏幕上 茄靠。(準(zhǔn)確來說是 *out*
輸出流)
函數(shù)像是一個黑盒子,你往里扔參數(shù)蝶桶,他向你扔出返回值嘹黔。
假如除此之外,這個黑盒子還打了你一巴掌莫瞬,那這一巴掌就是函數(shù)的“副作用”儡蔓。
如果你是為了得到你的返回值,那這個函數(shù)的“功能”就是返回的這個值疼邀。如果你想要享受痛苦喂江,那這一巴掌就是他的“功能”。
這里我們顯然利用的是 print
函數(shù)的副作用旁振,對我們來說它才有用获询。
而 print
函數(shù)的返回值永遠(yuǎn)為 nil
,所以也就不那么重要了拐袜。
Clojure 試圖求值一切
函數(shù)的值等于它的返回值吉嚣,而字符串的值就簡單的等于他看起來的樣子。
(雙引號 ""
中的內(nèi)容稱之為字符串蹬铺,它可以用來存儲簡單的文字或者數(shù)據(jù)尝哆,是程序設(shè)計語言中非常常見的 “明星” 。)
你可能對上面這一大堆話并不是很理解
沒關(guān)系甜攀,我們多看例子
比如我們可以給 print
函數(shù)更多的參數(shù)
=> (print "hello world!" "hello again!" "bye!")
hello world! hello again! bye!
nil
或者一個參數(shù)也不給它
=> (print)
nil
觀察結(jié)果
我們看到 print
函數(shù)果然顯示了它的副作用 --- 依次顯示每個參數(shù)的值秋泄。
例外地,如果沒有參數(shù)规阀,它自然也就沒有副作用可以被觸發(fā)恒序。
最后,它的返回值 nil
總是在最后一行被顯示谁撼。
Clojure 的“括號表示法”是可以嵌套的
=> (print (print "I love Rock!!!"))
I love Rock!!!nil
nil
為什么會出現(xiàn)這種結(jié)果呢歧胁?
重復(fù)一遍,Clojure 試圖求值一切內(nèi)容
函數(shù)的值是它的返回值厉碟,字符串的值是它本身…
這個例子的執(zhí)行步驟是這樣的
- 從左往右喊巍,找到第一個括號要執(zhí)行的函數(shù)為
print
-
print
函數(shù)的副作用是打印每個參數(shù)的值 - 但是這個參數(shù)的值無法直接確定,因為它并不是一個可以被直接求值的東西 --- 它又是一個函數(shù)墨榄。而函數(shù)也是有值的玄糟,函數(shù)的值就是它的返回值!
- 程序轉(zhuǎn)而執(zhí)行內(nèi)層的
(print "I love Rock!!!")
袄秩。字符串的值可以直接被得到阵翎。所以內(nèi)層print
函數(shù)發(fā)現(xiàn)它所有的參數(shù)都可以直接被求值逢并。于是它就開始發(fā)揮它的副作用了 --- 把每個參數(shù)的值打印出來,I love Rock!!! 就顯示出來了郭卫。 - 此時內(nèi)層函數(shù)的值確認(rèn)了 --- **內(nèi)層
print
函數(shù)的值等于它的返回值nil
**(雖然你一眼就能知道返回值永遠(yuǎn)為nil
砍聊,但計算機程序沒有這個本事,它只能執(zhí)行之后才能知道) - 外層函數(shù)發(fā)現(xiàn)內(nèi)層所有的參數(shù)都已經(jīng)求值完畢贰军,
(如果這個時候時間靜止的話玻蝌,由于內(nèi)層的“謎題”已經(jīng)被解開,那我們的代碼可能就會變成像這個樣子)
(print nil)
此時外層 print
函數(shù)的副作用發(fā)生词疼!輸出每個參數(shù)的值俯树,即輸出內(nèi)層函數(shù)的值 --- nil
。
- 最后外層函數(shù)返回值
nil
顯示在屏幕上贰盗。
如果你使用一些集成開發(fā)環(huán)境许饿,那么你可以看到 print
函數(shù)的副作用所顯示的 nil
和print
函數(shù)的返回值 nil
的顯示效果(如顏色和字體)看起來是不同的
一整句嵌套的表達(dá)式的返回值只有一個!它取決于最外層的那個函數(shù)的返回值舵盈!此例中即為最外層的那個print
的值 nil
同樣陋率,你可能對上面這一大堆話并不是很理解
我們再來幾個例子
這次來介紹一個新的函數(shù) println
它與print
函數(shù)的唯一不同在于,每次產(chǎn)生副作用打印時秽晚,自動在末尾換行
=> (println (println "I love Rock!!!"))
I love Rock!!!
nil
nil
=> (println (print "I love Rock!!!") (println "I love Rock too!!!") (print "I love you..."))
I love Rock!!!I love Rock too!!!
I love you...nil nil nil
nil
可以看到瓦糟,最外層 println
函數(shù)在等待所有參數(shù)的值依次求值完畢后,副作用發(fā)生赴蝇,一次性輸出了三個 nil
菩浙,然后顯示了自己的返回值
函數(shù)返回值是自動換行顯示的(有些 REPL 環(huán)境并不自動換行,取決于具體實現(xiàn))扯再,println
函數(shù)的換行效果指的是在副作用的末尾換行芍耘,即打印完畢后換行,此例中是在 "I love Rock too!!!"
后換了一行
作為一個程序設(shè)計語言熄阻,計算自然是最基礎(chǔ)的。
但與其它語言或者日常習(xí)慣不同的一點倔约,Clojure 的計算表示使用前綴表達(dá)式秃殉,即“運算符”放在“操作數(shù)”之前。
在 Clojure 里浸剩,運算符號同樣是個普通的函數(shù)(甚至不是一個關(guān)鍵字)
而函數(shù)理所當(dāng)然要放在括號的第一個位置
=> (+ 1 1)
2
加法函數(shù) +
接收任意數(shù)量的表達(dá)式作為參數(shù)钾军,它的返回值是各個參數(shù)的和,它沒有副作用绢要。
同樣吏恭,加法函數(shù)是可以嵌套的
=> (+ 3 (+ 1 22))
26
等價于
=> (+ 3 1 22)
26
在上面的嵌套示例中, +
兩個參數(shù)是 3
和 (+ 1 22)
這兩個表達(dá)式重罪。(數(shù)字 3
也是一個正確的表達(dá)式)這兩個表達(dá)式之間最好加上一個空格樱哼,這樣會使得代碼的層次感更好哀九,也遵循了參數(shù)之間使用空格隔開的規(guī)則。
與之前的例子相似搅幅,在遇到有參數(shù)需要進(jìn)一步求值時阅束,會先求內(nèi)層的值
這種做法使得你無需記憶無趣又無用的運算優(yōu)先級
因為每個運算符號一定在括號的第一個位置,所以你總是能一層一層的找到唯一的計算順序
=> (+ 2 (* 8 2));等價于中綴表達(dá)式 2 + 8 * 2
18
=> (* 2 (+ 8 2));等價于中綴表達(dá)式 2 * (8 + 2)
20
現(xiàn)在你已經(jīng)初步了解了 Clojure 的執(zhí)行過程與它的語法
接下來你會逐漸適應(yīng)這種看似奇怪的表達(dá)方式
最終陶醉于這種表達(dá)方式所帶來的優(yōu)雅茄唐、簡潔和便利
以及這種強大的語言所產(chǎn)生的無法抗拒的魅力
-
REPL 即 Read-Eval-Print Loop --- “讀取-求值-輸出” 循環(huán) ?
-
表達(dá)式:你可以簡單理解為一段可以被 Clojure 所執(zhí)行的代碼 ?
-
副作用(Side effect):副作用是指息裸,表達(dá)式被求值后,對外部世界的狀態(tài)做的某些改變沪编。當(dāng)我們對一個如 (+ 1 2)
這樣純粹的 Lisp 表達(dá)式求值時呼盆,沒有產(chǎn)生副作用。它只返回一個值蚁廓。但當(dāng)我們調(diào)用 print 時宿亡,它不僅返回值,還印出了某些東西纳令。這就是一種副作用挽荠。(引用自ANSI Common Lisp 中文翻譯版) ? -
返回值即為表達(dá)式執(zhí)行后的值,同時是表達(dá)式本身的值平绩,Clojure 中所有的表達(dá)式都有值 ?