Clojure
零基礎(chǔ)
學習筆記
函數(shù)式編程
函數(shù)即是值。
終于甘凭,我們要介紹 Clojure 中最重要的部分了。
在此之前丹弱,你已經(jīng)見到好多 Clojure 自帶的函數(shù)了,比如打印函數(shù) print
家族躲胳,給值取名的函數(shù) def
蜓洪,還有一些對集合操作的函數(shù) first
assoc
…
你可能已經(jīng)熟記了函數(shù)的幾個特征:
- 函數(shù)都有返回值
- 函數(shù)可以接受參數(shù)隆檀,也可以不接受參數(shù)
- 如果函數(shù)改變了外部世界的一些狀態(tài)(如修改了某個值,或者改變了屏幕上的顯示效果)粹湃,我們稱這個函數(shù)存在副作用
小貼士:始終要記住,在 Clojure 中为鳄,函數(shù)總是能被求值,函數(shù)的值即為他的返回值济赎。
所以當存在好多小括號進行嵌套的時候,我們可以分別求出內(nèi)層函數(shù)的值司训,然后再一層層的計算出整體的值液南,從而得到程序的結(jié)果。
雖然 Clojure 內(nèi)置了豐富的函數(shù)滑凉,但很多情況下喘帚,我們還是需要創(chuàng)造出自己的函數(shù),或者組裝拼接一些現(xiàn)有函數(shù)吹由,使之更為強大。
定義函數(shù)
首先朱嘴,讓我們從創(chuàng)造一個簡單的函數(shù)開始說起,
我們可以使用 fn
函數(shù)來創(chuàng)建一個屬于我們自己的函數(shù)萍嬉,
我們這個自定義函數(shù)的功能是,它接受一個數(shù)字作為參數(shù)壤追,返回這個數(shù)字加上10之后的值。
=> (fn [x]
(+ x 10))
#<core$eval8059$fn__8060 my_clojure_study.core$eval8059$fn__8060@20302ac5>
看起來和我們之前接觸過的 let
函數(shù)有點類似行冰,沒錯,fn
函數(shù)隱式使用了 let
函數(shù)悼做。也就是你可以在那個中括號里面使用之前我們學到的關(guān)于解構(gòu)的知識!
-
fn
函數(shù)的第一個參數(shù)贿堰,用中括號包圍辙芍,用來表示我們自定義函數(shù)的參數(shù)列表故硅。
比如這里的x
。 - 而
fn
函數(shù)接下來的參數(shù)是一系列表達式纵搁,它們會被依次執(zhí)行。最后一個被執(zhí)行的表達式則作為自定義函數(shù)的返回值腾誉。 -
fn
函數(shù)本身的返回值,是我們的自定義函數(shù)本身利职。這里返回的是一串看似無意義的隨機函數(shù)名,我們無視它猪贪。(還記得我們一開始所說的,“函數(shù)即是值么”热押,這意味著我們可以把函數(shù)作為返回值來返回N骺)
我們可以這樣來使用我們創(chuàng)造出的函數(shù):[1]
=> ((fn [x] (+ x 10)) 1)
11
是不是有點暈?
不要急拥褂,我們來仔細看一下。
首先饺鹃,(fn [x] (+ x 10))
的值是一個函數(shù),這個函數(shù)的功能就是我們自定義的 --- 它接受一個數(shù)字尤慰,返回這個數(shù)字加上10的結(jié)果。
由于我們的自定義函數(shù)放在了外層括號的第一個位置伟端,所以它會當作函數(shù)來執(zhí)行,而之后的數(shù)字1责蝠,則作為我們自定義函數(shù)的參數(shù)党巾。
數(shù)字1傳到了 x
的位置霜医,所以此時 x
的值就成了 1。
執(zhí)行自定函數(shù)內(nèi)的代碼 (+ x 10)
肴敛,由于 x
現(xiàn)在為1,此時我們的代碼就等價于 (+ 1 10)
医男。
計算出結(jié)果11,返回镀梭。
還是不明白?不用擔心报账,這次我們使用之前學過的 def
函數(shù)來給我們的“新朋友”取一個名字研底。
=> (def plus-ten (fn [some-num] (+ some-num 10)))
#'my-clojure-study.core/plus-ten
(我們把我們的自定義參數(shù)的參數(shù)名 x
換了一個新名字榜晦,這樣并不會影響函數(shù)的功能,但是會增加少許可讀性琐凭。)
現(xiàn)在我們的自定義函數(shù)就有了一個新名字 --- plus-ten
然后我們就可以這樣來使用它了:
=> (plus-ten 24)
34
看起來工作的還不錯。
由于我們經(jīng)常需要定義諸如此類的有名字的自定義函數(shù)统屈,
所以 Clojure 給我們內(nèi)置了一個 def
和 fn
的合體函數(shù)! --- defn
很好記吧愁憔,它是這么來使用的:
=> (defn plus-ten
[some-num]
(+ some-num 10))
#'my-clojure-study.core/plus-ten
=> (plus-ten 45678)
45688
這里我們又使用了換行,來把參數(shù)列表和函數(shù)體分隔開吨掌,顯得程序?qū)哟胃忧逦?br> 如你所見:
-
defn
函數(shù)的第一個參數(shù)是我們給自定義函數(shù)取的名字。 - 第二個參數(shù)用中括號包圍膜宋,里面是我們自定義函數(shù)的參數(shù)列表。
- 之后的代碼是我們自定義函數(shù)的主體部分秋茫,并把最后一個作為自定義函數(shù)的返回值(這里只有一個史简,所以就把它作為返回值)肛著。
使用起來非常方便。
解構(gòu)參數(shù)列表
之前我們提到枢贿,fn
和 defn
都隱式使用了 let
,所以它們都支持解構(gòu)局荚。
下面我們來看一下具體的例子。
自定義函數(shù)所接受的參數(shù)可以不是單調(diào)的數(shù)字之類耀态,我們可以直接接受一個列表。
假如我們想寫一個自定義函數(shù)茫陆,它的功能是:
接受一個列表金麸,把這個列表中第一個值和第三個值相加挥下,并把結(jié)果返回。
例如桨醋,如果接受 (1 2 4)
或者 [1 2 4]
,它應(yīng)該返回5喜最。
我們可以這樣來實現(xiàn):
=> (defn plus-first-and-third
[[f _ t]]
(+ f t))
#'my-clojure-study.core/plus-first-and-third
=> (def some-collection [1 2 4])
#'my-clojure-study.core/some-collection
=> (plus-first-and-third some-collection)
5
這里和我們前面學的解構(gòu)形式略有不同。defn
函數(shù)的解構(gòu)首先直接對應(yīng)了參數(shù)傳入的位置,如此例中的第一參數(shù)迷雪,然后對這個位置再寫一個中括號進行解構(gòu)。
舉個栗子章咧。
如果傳入了三個參數(shù) [x y z]
,我們要解構(gòu) y
赁严,
那么就可以寫成 [x [first-y second-y] z]
。
這個例子中疼约,plus-first-and-third
函數(shù)接受一個參數(shù),
而這個參數(shù)又被解構(gòu)成三個值 --- f
_
t
程剥。
還有一種常見的解構(gòu)形式是劝枣,解構(gòu)剩余參數(shù)倡缠。
我們在綁定與解構(gòu)一文的最后,介紹了使用 &
給剩余元素取名.
這使得我們可以創(chuàng)建不定長的昙沦,可變參數(shù)列表。
比如我們創(chuàng)建這樣一個函數(shù)盾饮,
它接受大于三個參數(shù),打印前兩個參數(shù)的和的值丘损,以及打印后續(xù)參數(shù)。返回 nil
徘钥。
我們可以這樣實現(xiàn)它:
=> (defn print-plus-f-s
[f s & rest-num]
(println (+ f s))
(println rest-num))
#'my-clojure-study.core/print-plus-f-s
多于3個的參數(shù)會被作為一個 list
來綁定到 rest-num
衔蹲。
現(xiàn)在看看它是如何工作的:
=> (print-plus-f-s 1 2 3 4 5)
3
(3 4 5)
nil
它打印了1和2的和呈础,并且打印了后續(xù)數(shù)字的 list
形式。
由于最后一個表達式的值被作為自定義函數(shù)的值而钞,而 println
函數(shù)的值始終為 nil
,所以我們的 print-plus-f-s
函數(shù)的返回值始終為 nil
臼节。
實際上珊皿,剛才我們討論的剩余參數(shù)一樣可以繼續(xù)被解構(gòu):
=> (defn print-plus-f-s
[f s & [third-num & rest-num]]
(println (+ f s))
(println third-num)
(println rest-num))
#'my-clojure-study.core/print-plus-f-s
=> (print-plus-f-s 1 2 3 4 5)
3
3
(4 5)
nil
解構(gòu)是一個非常實用的技能,但是多層的解構(gòu)對于初學者來說理解起來較為困難蟋定。
不過不要灰心,稍微復雜的例子留給大家仔細思考一下垢夹。
試著自己寫出一些代碼并運行是幫助學習的有效途徑维费。
如果實在是找不到解答,可以在下方留言犀盟。
高階函數(shù)
函數(shù)和數(shù)字而晒、字符串阅畴、列表之類的數(shù)據(jù)一樣,可以直接作為參數(shù)或者返回值來進行傳遞贱枣。
我們把接受函數(shù)為參數(shù),或者把函數(shù)作為返回值的函數(shù)稱之為高階函數(shù)纽哥。
使用高階函數(shù),我們就能組裝出更為強大和靈活的武器春塌。
例如我們剛才學到的 fn
函數(shù)晓避,就是一個高階函數(shù)只壳,因為它的返回值是我們的自定義函數(shù)。
我們同樣可以創(chuàng)造出屬于我們自己的高階函數(shù)吼句。
剛才我們創(chuàng)建了一個 plus-ten
的函數(shù),它可以把一個數(shù)字加上10惕艳,并把這個結(jié)果返回過來。但是此時你的老板覺得我們還需要一個把一個數(shù)字減去10的函數(shù)尔艇。(雖然這個例子很幼稚尔许,你大可以直接使用減法终娃,但是我們只是以此來說明高階函數(shù)的簡單用法。)
你可以再創(chuàng)造一個函數(shù):
=> (defn subtract-ten
[some-num]
(- some-num 10))
#'my-clojure-study.core/subtract-ten
但你可惡的老板(或者你的客戶)又要求你寫一個乘以10的函數(shù),鬼知道他會不會再讓你寫更多的無聊函數(shù)柠新。
如果有一個函數(shù)可以根據(jù)參數(shù)來生成不同的函數(shù),那你就可以從無聊的重復代碼上解脫了辉巡。
高階函數(shù)就可以用來做這個:
=> (defn operate-ten
[operate]
(fn [x] (operate x 10)))
#'my-clojure-study.core/operate-ten
這是一個可以生成函數(shù)的函數(shù),它接受一個函數(shù)作為參數(shù)郊楣,同時返回一個函數(shù)。
而它所返回的函數(shù)净蚤,則使用 operate-ten
所接受的那個參數(shù)來作為返回函數(shù)的執(zhí)行部分的運算符。
說人話
好吧今瀑,我們直接看看它是怎么工作的:
=> (def subtract-ten (operate-ten -))
#'my-clojure-study.core/subtract-ten
=> (subtract-ten 9)
-1
- 首先程梦,我們給這個能生成函數(shù)的函數(shù)傳參數(shù)
-
橘荠,也就是減法函數(shù)。 - 然后哥童,operate 這個值就變成了我們的減法函數(shù)
-
。
如果時間停止如蚜,展開operate-ten
這個高階函數(shù),那么它是這個樣子的:
=> (defn operate-ten
[-]
(fn [x] (- x 10)))
沒錯错邦!值就這樣簡單的被傳進來的參數(shù)替換了而已探赫!
- 時間繼續(xù)流逝撬呢,現(xiàn)在它返回了
(fn [x] (- x 10))
。這正是我們想要的函數(shù)魂拦! - 然后我們使用
def
函數(shù)來命名這個由函數(shù)生成的函數(shù)。 - 使用這個函數(shù)芯勘,就如同之前一樣。
哈哈荷愕,我們再也不用自己從頭寫一次代碼了衡怀!
這個時候你的老板要求你寫一個把一個數(shù)字乘以10的函數(shù),你就可以這樣來使用我們的高階函數(shù):
=> (def multiply-ten (operate-ten *))
#'my-clojure-study.core/multiply-ten
完事兒 =v=
僅此而已么抛杨?
當然不是!
我們甚至可以讓這個高階函數(shù)生成一個功能為:
“生成一個元素和10組成的有兩個元素的列表”的函數(shù)怖现。
=> (def list-with-ten-end (operate-ten list))
#'my-clojure-study.core/list-with-ten-end
=> (list-with-ten-end "我是第一個元素")
("我是第一個元素" 10)
或者:
“生成一個以10結(jié)尾的字符串”的函數(shù)。
=> (def string-with-ten-end (operate-ten str))
#'my-clojure-study.core/string-with-ten-end
=> (string-with-ten-end 123)
12310
=> (string-with-ten-end "head-")
head-10
小貼士:
str
函數(shù)可以把它的參數(shù)拼接成一個字符串屈嗤,并以此作為它的返回值潘拨。
例子:
=> (str "hello" "world!" 10)
helloworld!10
僅僅因為恢共,我們向 operate-ten
所傳遞的函數(shù)的不同!(這里是 list
和 str
)
此次我們學習了作為 Clojure 中“頭等公民”的函數(shù)讨韭。
了解了自定義函數(shù)的幾種聲明方式捣鲸,以及參數(shù)列表的常見解構(gòu)形式渴逻。
最后我們窺見了函數(shù)式編程中高階函數(shù)的概念祈远,并體驗了它所帶來的靈活性蔫耽。
最后的最后控乾,我希望你能仔細理解本文中的代碼幔欧,最好實際運行一下,并嘗試修改它們礁蔗。
-
這里你可能注意到,我們使用了
x
來表示我們的參數(shù)浴井。在其他語言中,這種命名風格通常是不被建議的磺浙。但是在 Clojure 中洪囤,你可以使用類似x
y
a
b
之類的名稱撕氧,來表示一個非常通用的類型,也就是表示這個函數(shù)支持各種類型的值伦泥。 ?