├── 00.md ├── 01.md ├── 02.md ├── 03.md ├── 04.md ├── 05.md ├── 06.md ├── 07.md ├── 08.md ├── 09.md ├── 11.md ├── 12.md ├── 14.md ├── LICENSE └── README.md /00.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | > 世界太新,很多事物還沒有名字,必須用手指頭伸手去指。 4 | > 5 | > — 賈西亞•馬奎茲《百年孤寂》 6 | 7 | ## Clojure 是什麼 8 | 9 | Clojure 從 2007 年由創始人 Rich Hickey 公開發表以來,至今已經經過十個年頭。經過十年歲月的演進,Clojure 程式語言已經被越來越多公司採用,也有許多優秀的開源專案選擇使用 Clojure,是成熟與實用的程式語言。 10 | 11 | Clojure 是動態程式語言,意思是指在 Clojure 裡頭,撰寫者可以在執行時期隨時改變構成程式的元素,例如物件、型態或函式,擁有比靜態更多的彈性與自由。 12 | 13 | 除了是動態程式語言之外,Clojure 也是一個函數式程式語言。如果搜尋網路上函數式程式語言的定義,可以找出每個人都有不同的看法。但是基本的看法是不變的,那就是函式能夠被當成基本型態一樣自由地建立、可以當成另一個函式的參數傳遞、也可以被當成函式的返回值,傳遞給上層的函式呼叫者。 14 | 15 | Clojure 屬於 LISP 程式語言家族的一員,亦即它們有相似的語法以及「程式如資料」設計理念。從外觀上看起來就是一堆括號與關鍵字組合而成。LISP 程式語法使用前置表示法,將運算元或函式名稱擺放在括號中的第一個位置,其他位置則擺放參數或運算子。有其他程式語言經驗的人初次遇見時,可能會因爲它相異於目前主流的語法而卻步,然而如果花點時間耐心學習,將會理解到 LISP 語法其實非常簡單,但是在簡單的語法之中還能有複雜的變化,搭配強大的巨集系統,足以構建複雜的大型軟體系統。 16 | 17 | Clojure 也是一個寄宿在 JVM 的程式語言,利用 JVM 成熟的虛擬化以及動態編譯技術,和數以萬計爲 JVM 編寫的成熟函式庫,能夠開發穩定可靠的軟體系統。雖然也有使用 .NET 技術的 Clojure 可供使用,但是目前的成熟程度仍然無法與使用 JVM 技術的 Clojure 相比。 18 | 19 | Clojure 另外一個特點則是提供了方便簡單的並行程式設計方法,讓撰寫程式的人不必再去處理複雜的並行問題,專心處理核心問題。搭配設計精巧的語言特性,撰寫程式的人亦可以簡單地將程式遷移到具備多核心處理器的環境之中,充分發揮多核心處理器平行運算的能力。 20 | 21 | Clojure 還提供了豐富好用的資料結構,這些資料結構都是不可變以及持久存在的。不可變指的是一個資料結構一旦建立之後,就無法再修改它,必須再建立新的資料結構存放新的資料;持久存在則是 Clojure 在建立新資料時會參考舊資料的內容,從舊資料中分支出的新的資料結構。這樣的方法有效地利用了空間,並減少了重新建立資料的時間。 22 | 23 | ## 適合的讀者 24 | 25 | 如果你是想要了解學習 Clojure 的讀者,這一系列的文章就是爲你而寫的。雖然這一系列文章是寫給初學 Clojure 的讀者,但是如果你有其他程式設計語言的經驗,例如物件導向程式語言 Python 或 Ruby,這一系列的文章便非常的適合你。如果你完全是一個程式設計語言的新手,建議你搭配其他市面上一般程式語言學習的書籍一起閱讀。 26 | 27 | 如果你已經是個非常有經驗的程式設計老手,想要鑽研 Clojure 內部結構或是精妙的設計手法,這一系列的文章並不適合你,建議你可以另外再去閱讀更專門的書籍。 28 | 29 | ## 如何使用 30 | 31 | 建議的閱讀方式是從頭開始閱讀起,有助於循序地了解 Clojure 的整體樣貌,在文章之中亦會穿插示範用的程式碼或操作,希望閱讀的各位讀者都可以在電腦上親自輸入並觀察結果。除了藉由輸入程式碼體會 Clojure 程式的樣貌之外,更可以在輸入並執行的過程中,了解還有哪些知識需要再補強。 32 | 33 | 在閱讀此一系列文章時候,或許因爲與過往學習的程式語言不同,而有卡關不理解的地方。這時候請你放慢腳步並保持耐心,畢竟新的思維並不是那麼容易就可以體會,但是一旦了解之後,定是非常舒暢愉快。 34 | 35 | 還有就是不要害怕括號。Clojure 程式中有很多的括號,那其實是 LISP 程式語言家族表現程式碼的方法,非常的單純與簡潔。當你多花時間體會之後,你將會對這種程式語言的表現手法感到讚歎!搭配編輯工具的套件來使用,輸入或修改括號不再是繁瑣無趣的工作。 36 | 37 | ## 使用慣例 38 | 39 | 在本系列文章中,有時候會在命令列模式下輸入指令,例如: 40 | 41 | ```sh 42 | $ java --version 43 | ``` 44 | 45 | 其中的 `$` 符號代表命令列模式下的提示符號,是不需要實際輸入的,只有 `$` 符號之後的文字才需要輸入。 46 | 47 | 除了命令列的提示符號之外,本系列文章中還會讓讀者在 REPL 之下輸入程式碼,例如: 48 | 49 | ```clojure 50 | (+ 1 2) 51 | ;; => 2 52 | ``` 53 | 54 | 分號在 Clojure 程式語言中代表的是註解區域的開頭,所有分號之後的文字將被忽略不會當作程式執行。在這裡表示 Clojure 程式運行之後的結果。 55 | 56 | ## 該準備的工具 57 | 58 | 這裡將會假設讀者你已經有一臺電腦可以使用,並且電腦中搭載了你熟悉的編輯器。還會假設你不會害怕在命令列模式下操作,因爲有很多時候將會在命令列模式下輸入程式碼。 59 | 60 | ### 安裝 Java 61 | 62 | 因爲 Clojure 寄宿在 JVM 之上,所以你使用的電腦需要安裝 Java,如果不確定你的電腦是否安裝了 Java,你可以在命令列下輸入: 63 | 64 | ```sh 65 | $ java --version 66 | ``` 67 | 68 | 如果出現類似以下的訊息,表示你的環境已經安裝了 Java,你可以到下一個步驟安裝 Leiningen: 69 | 70 | ```sh 71 | java version "1.8.0_71" 72 | Java(TM) SE Runtime Environment (build 1.8.0_71-b15) 73 | Java HotSpot(TM) 64-Bit Server VM (build 25.71-b15, mixed mode) 74 | ``` 75 | 76 | Java 版本必須要在 1.6 以上才可正常運作。 77 | 78 | 如果沒有安裝 Java,請到 [Java 官方網站](https://goo.gl/Le1Qpx) 下載並安裝。 79 | 80 | ### 安裝 Leiningen 81 | 82 | Leiningen 是 Clojure 的專案管理工具,類似 Java 中的 Maven 或 Node.js 中的 npm。首先到 [Leiningen 官方網站](https://goo.gl/rmK2vh) 遵從指示安裝之後,在命令列上輸入以下指令: 83 | 84 | ```sh 85 | $ lein --version 86 | ``` 87 | 88 | 如果出現類似以下的訊息,代表你的 Leiningen 已經安裝成功: 89 | 90 | ```sh 91 | Leiningen 2.8.1 on Java 1.8.0_71 Java HotSpot(TM) 64-Bit Server VM 92 | ``` 93 | 94 | 請注意,第一次執行 Leiningen 時因爲還會下載額外的套件,所以需要比較多的時間,請耐心等待。 95 | 96 | ### 建立專案 97 | 98 | 當你的電腦已經安裝了 Java 與 Leiningen 之後,便可以開始建立專案了。在命令列模式之下輸入: 99 | 100 | ```sh 101 | $ lein new embracing-clojure 102 | ``` 103 | 104 | Leiningen 便會開始建立一個目錄名稱爲 embracing-clojure,目錄中已經預先放置了一些程式碼與專案的設定檔。再來切換到專案的目錄底下: 105 | 106 | ```sh 107 | $ cd embracing-clojure 108 | ``` 109 | 110 | 切換目錄之後,執行以下指令開啓 REPL: 111 | 112 | ```sh 113 | $ lein repl 114 | ``` 115 | 116 | 出現類似以下訊息就表示 REPL 已成功開啓: 117 | 118 | ```sh 119 | nREPL server started on port 50753 on host 127.0.0.1 - nrepl://127.0.0.1:50753 120 | REPL-y 0.3.7, nREPL 0.2.12 121 | Clojure 1.8.0 122 | Java HotSpot(TM) 64-Bit Server VM 1.8.0_71-b15 123 | Docs: (doc function-name-here) 124 | (find-doc "part-of-name-here") 125 | Source: (source function-name-here) 126 | Javadoc: (javadoc java-object-or-class-here) 127 | Exit: Control+D or (exit) or (quit) 128 | Results: Stored in vars *1, *2, *3, an exception in *e 129 | 130 | user=> 131 | ``` 132 | 133 | 你可以試着在其中輸入程式碼 ```(+ 1 2)``` 並按下 Enter 按鍵,你將會看到以下的訊息: 134 | 135 | ```sh 136 | nREPL server started on port 50753 on host 127.0.0.1 - nrepl://127.0.0.1:50753 137 | REPL-y 0.3.7, nREPL 0.2.12 138 | Clojure 1.8.0 139 | Java HotSpot(TM) 64-Bit Server VM 1.8.0_71-b15 140 | Docs: (doc function-name-here) 141 | (find-doc "part-of-name-here") 142 | Source: (source function-name-here) 143 | Javadoc: (javadoc java-object-or-class-here) 144 | Exit: Control+D or (exit) or (quit) 145 | Results: Stored in vars *1, *2, *3, an exception in *e 146 | 147 | user=> (+ 1 2) 148 | 3 149 | user=> 150 | ``` 151 | 152 | REPL 是 Read Evaluation Print Loop 的簡稱,在 REPL 環境會將你輸入的程式碼讀入 (Read),對程式碼求值 (Eval) 取得結果之後,把結果輸出到顯示裝置 (Print),並一再地重複這一系列的動作 (Loop)。在 REPL 中不僅可以將腦中的想法一步步地輸入驗證,亦可以利用 REPL 測試已經寫好的程式碼,快速且即時的回應將使得程式更快的驗證,確保程式的正確。 153 | 154 | 恭喜你!你已經成功地執行了第一個 Clojure 程式! 155 | 156 | ## 回顧 157 | 158 | 從本篇文章中你已經知道 Clojure 的歷史與特性,知道它是建立在 JVM 上的語言並支持並行設計。也知道 Clojure 屬於 LISP 程式語言家族的一份子,還有 Clojure 的資料結構都是不可變的,不用再擔心哪邊的程式修改了不該修改的資料。 159 | 160 | 你也已經下載並安裝了 Java 與 Leiningen,利用 Leiningen 建立了第一個 Clojure 專案,還在 REPL 中輸入了第一個 Clojure 運算式。 161 | 162 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 163 | -------------------------------------------------------------------------------- /01.md: -------------------------------------------------------------------------------- 1 | # 基本組成 2 | 3 | ## 運算式 4 | 5 | Clojure 程式是由許多運算式 (Expression) 組合而成。在 Clojure 中,Expression (運算式) 也被稱爲 Form (形式)。一個運算式執行之後 (或叫做 Evaluate 求值),得到執行後的結果。 6 | 7 | 以 Hello World 範例程式爲例,以下是 Clojure 版本: 8 | 9 | ```clojure 10 | (str "Hello " "World") 11 | ;; => "Hello World" 12 | ``` 13 | 14 | 以上的運算式使用列表 (List) 來表示函式呼叫,列表使用左右兩個小括號來表示。括號中分別有三個元素:符號 (Symbol) ```str```,以及兩個字串:```Hello ``` 與 ```World```。 15 | 16 | 符號 (Symbol) ```str```,對應到 Clojure 內建的函式。Clojure 會找到這個符號對應的函式並呼叫它,執行的結果是將帶入的字串串接起來。 17 | 18 | Clojure 與 LISP 家族跟其他語言不同的是,語法採用前置表示法 (或稱做波蘭表示法),將函式或運算元擺放在括號內的第一個位置,之後的位置則擺放各個參數。有些情況下第一個位置擺放的並非函式或運算元,這種特殊的運算式被稱爲 Special forms。 19 | 20 | 在其他非使用前置表達式的語法中,要將一連串數字相加起來會寫成: 21 | 22 | ```python 23 | 1 + 2 + 3 + 4 + 5 24 | ``` 25 | 26 | 而使用前置表達式的 Clojure 只要寫成: 27 | 28 | ```clojure 29 | (+ 1 2 3 4 5) 30 | ``` 31 | 32 | 或是在其他語言中的 ```+``` 與 ```*``` 的執行優先順序需要注意,一搞錯結果就會不一樣: 33 | 34 | ```python 35 | 1 + 2 * 3 36 | ``` 37 | 38 | 使用前置表達式就非常清楚,誰先處理後處理則一目瞭然: 39 | 40 | ```clojure 41 | (+ 1 (* 2 3)) 42 | ``` 43 | 44 | ## 資料型態 45 | 46 | Clojure 提供跟其他主流語言類似的資料型態與資料結構,而有些則與主流程式語言不盡相同,這裡先提供大致的導覽,之後文章將會詳細介紹。 47 | 48 | ### 數字 49 | 50 | Clojure 提供了跟主流語言類似數字表示法,常用的有整數、浮點數和有理數。 51 | 52 | 整數: 53 | 54 | ```clojure 55 | 42 56 | ;; => 42 57 | ``` 58 | 59 | 浮點數: 60 | 61 | ```clojure 62 | 3.14 63 | ;; => 3.14 64 | ``` 65 | 66 | 有理數: 67 | 68 | ```clojure 69 | (/ 1 3) 70 | ;; => 1/3 71 | ``` 72 | 73 | ### 字串 74 | 75 | Clojure 的字串使用雙引號方式來表示,引號內擺放需要的文字。型態是 Java 中的字串類型 java.lang.String: 76 | 77 | ```clojure 78 | "Issac Asimov" 79 | ;; => "Issac Asimov" 80 | ``` 81 | 82 | ### 字符 83 | 84 | Clojure 表現字符 (Character) 的方式與主流程式語言稍有不同,它在欲使用的字符之前加上反斜線 (\\): 85 | 86 | ```clojure 87 | \A 88 | ;; => \A 89 | \B 90 | ;; => \B 91 | \b 92 | ;; => \b 93 | \a 94 | ;; => \a 95 | ``` 96 | 97 | 除了可視字符之外,以下是其它特殊字符的使用方式: 98 | 99 | ```clojure 100 | \space 101 | ;; => \space 102 | \newline 103 | ;; => \newline 104 | \formfeed 105 | ;; => \formfeed 106 | \return 107 | ;; => \return 108 | \backspace 109 | ;; => \backspace 110 | \tab 111 | ;; => \tab 112 | ``` 113 | 114 | ### 布林 115 | 116 | Clojure 程式語言使用 ```true``` 和 ```false``` 來表示邏輯上的真與假: 117 | 118 | ```clojure 119 | true 120 | ;; => true 121 | false 122 | ;; => false 123 | ``` 124 | 125 | 除了 ```true``` 以及 ```false``` 之外,Clojure 還加入了 ```nil``` 表示不存在與虛無。當用在邏輯判斷時,```nil``` 跟 ```false``` 被當作邏輯上的假。 126 | 127 | ### 符號 128 | 129 | 符號用來指稱某種東西,例如前面提到的 ```str``` 和 ```+``` 用來表示函式與運算元。Clojure 類似於其他程式語言用來定義變數的方式,就是使用 ```def``` 定義一個符號以及它對應的事物。 130 | 131 | ```clojure 132 | (def clojurist "Bob") 133 | ;; => #'user/clojurist 134 | clojurist 135 | ;; => “Bob” 136 | ``` 137 | 138 | 使用 ```def``` 會建立符號 ```clojurist``` 連結到 ```"Bob"``` 字串。在 REPL 中看到結果 ```clojurist``` 加了 user 與斜線 (/),斜線前面的符號指的是 ```clojurist``` 的命名空間 (namespace)。在 REPL 中,預設的命名空間是 ```user```。 139 | 140 | ### 關鍵字 141 | 142 | 關鍵字 (Keyword) 與符號的功能類似,也是標識符號 (Identifier),但是關鍵字必須以冒號 (:) 爲開頭,而且關鍵字不代表其他資料,只代表它自己。通常跟映射 (Map) 搭配使用,作爲映射的索引鍵 (Key)。 143 | 144 | ```clojure 145 | :foo 146 | ;; => :foo 147 | :bar 148 | ;; => :bar 149 | {:Lisp "McCarthy" :Clojure "Hickey"} 150 | ;; => {:Lisp "McCarthy", :Clojure "Hickey"} 151 | ``` 152 | 153 | ### 正則表達式 154 | 155 | Clojure 將前面加上井號 (#) 的字串視爲正則表達式,型態爲 Java 中的 java.util.regex.Pattern: 156 | 157 | ```clojure 158 | (class #"[0-9A-Za-z]") 159 | ;; => java.util.regex.Pattern 160 | ``` 161 | 162 | 正則表達式與內建的函式,如 ```re-seq```、```re-find``` 與 ```re-match``` 一起搭配使用。 163 | 164 | ### 群集 165 | 166 | 當資料變多變雜時,會需要程式語言提供容器將相似的資料整理在一起。Clojure 提供四種群集型態 (Collections):列表 (List)、向量 (Vector)、映射 (Map) 與集合 (Set)。 167 | 168 | ```clojure 169 | ;; List 170 | '(1 2 3) 171 | ;; => (1 2 3) 172 | 173 | ;; Vector 174 | [1 2 3] 175 | ;; => [1 2 3] 176 | 177 | ;; Map 178 | {:author "Isaac Asimov" :title "I, Robot"} 179 | ;; => {:author "Isaac Asimov", :title "I, Robot"} 180 | 181 | ;; Set 182 | #{1 2 3 4} 183 | ;; => #{1 4 2 3} 184 | ``` 185 | 186 | ## 空白 187 | 188 | Clojure 使用空白分隔運算式中的各個元素,主流程式語言中則是使用逗號 (,)。其實也可以使用逗號,它的功能跟空白完全一樣,但是依照編寫的習慣,建議使用空白來區隔元素。逗號通常用來分隔映射裡的元素,以增加可讀性。 189 | 190 | ```clojure 191 | {:name "Catherine", :age 40} 192 | ``` 193 | 194 | ## 註解 195 | 196 | 以分號 (;) 開頭的文字被視爲單行註解,Clojure 會將它忽略不執行。如果想要撰寫多行註解,可以使用 ```comment```。 197 | 198 | ```clojure 199 | (+ 1 2) ; the result is 3 200 | ;; => 3 201 | (comment 202 | I have a dream that one day this nation will rise 203 | up, live out the truth meaning of its creed.) 204 | ;; => nil 205 | ``` 206 | 207 | ## 函式 208 | 209 | Clojure 是一個函數式語言,函式的定義與使用至關重要。通常使用 ```defn``` 來定義函式: 210 | 211 | ```clojure 212 | (defn hello [name] 213 | (str "Hello, " name)) 214 | ;; => #’user/hello 215 | ``` 216 | 217 | ```defn``` 的第一個參數用來表示函式的名稱,名稱之後則是新定義函數的參數,中括號裡代表各參數的名字,之後便是函式的本體 (Body)。當函式被呼叫時,本體中的表達式將會被求值,所得到的值就是此函式的返回值。 218 | 219 | ## 回顧 220 | 221 | 從本篇文章中你已經知道了 Clojure 可以表達的數字類型,還有字串與字符的表達方式;也了解用來判斷邏輯真假的布林型態、指稱事物的符號和關鍵字型態;還知道了四種集合型態:列表、向量、映射與集合。還知道了定義函式的方法。 222 | 223 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 224 | -------------------------------------------------------------------------------- /02.md: -------------------------------------------------------------------------------- 1 | # 資料結構與型態 2 | 3 | 本篇文章將介紹 Clojure 內建的的資料結構與型態,會先從簡單的資料型態如數字及字串開始,再介紹複雜的資料結構如群集 (Collection) 與序列 (Sequence)。 4 | 5 | ## 數字 (Number) 6 | 7 | ### 整數 8 | 9 | 在 Clojure 中,整數的表示法與主流程式語言無異,如果沒有特別聲明,預設爲十進位表示法。大小爲 64 位元有號整數,內部使用的型態爲 Java 中的 long: 10 | 11 | ```clojure 12 | (class 42) 13 | ;; => java.lang.Long 14 | 42 15 | ;; => 42 16 | -42 17 | ;; => -42 18 | ``` 19 | 20 | 除了十進位表示法之外,也提供了八進位、十六進位的表示法: 21 | 22 | ```clojure 23 | 0x2a 24 | ;; => 42 25 | 052 26 | ;; => 42 27 | ``` 28 | 29 | 在數字前面加上 0 被視爲八進位表示法、前面加上 0x 則是十六進位表示法。也可以自行決定數字的基底,只要在數字前面加上想要使用的基底 (範圍從 2 到 36),再加上 r 即可: 30 | 31 | ``` 32 | 2r101010 33 | ;; => 42 34 | 16r2A 35 | ;; => 42 36 | ``` 37 | 38 | ### 浮點數 39 | 40 | Clojure 的浮點數表示法也與主流程式語言無異,採用的是 IEEE 754 雙精度標準,大小爲 64 位元,內部使用的型態爲 Java 中的 double: 41 | 42 | ```clojure 43 | (class 3.14) 44 | ;; => java.lang.Double 45 | 3.14 46 | ;; => 3.14 47 | 1.618 48 | ;; => 1.618 49 | ``` 50 | 51 | 以指數的方式表現: 52 | 53 | ```clojure 54 | 3.14e-2 55 | ;; => 0.0314 56 | +1.618e-1 57 | ;; => 0.1618 58 | ``` 59 | 60 | ### 有理數 61 | 62 | Clojure 爲了支持高精度的計算,提供了有理數型態。舉例來說,在其他主流程式語言裡,1/3 的結果爲 0.3333…,或是將浮點數相加起來,原有的精確度反而在計算中喪失了: 63 | 64 | ```python 65 | >>> 1.0 / 3.0 66 | 0.3333333333333333 67 | >>> 0.1 + 0.1 + 0.1 68 | 0.30000000000000004 69 | ``` 70 | 71 | 使用有理數型態作運算,不會經過不必要的轉換而喪失準確度,只有在需要的時候,由使用者決定是否該轉換型態。內部使用的型態爲 clojure.lang.Ratio。 72 | 73 | ```clojure 74 | (class (/ 1 3)) 75 | ;; => clojure.lang.Ratio 76 | (/ 1 3) 77 | ;; => 1/3 78 | (+ 1/10 1/10 1/10) 79 | ;; => 3/10 80 | ``` 81 | 82 | 你可以將有理數轉型成浮點數: 83 | 84 | ```clojure 85 | (double 1/3) 86 | ;; => 0.3333333333333333 87 | ``` 88 | 89 | 也可以將浮點數轉型成有理數: 90 | 91 | ```clojure 92 | (rationalize 0.3) 93 | ;; => 3/10 94 | ``` 95 | 96 | ### 大數 97 | 98 | 一般來說,預設提供的 64 位元整數與浮點數已經綽綽有餘,但是如果需要處理超過 64 位元範圍的數值,就需要使用到 Clojure 提供的兩個大數型態:大整數 (BigInt)、大浮點數 (BigDecimal)。內部使用的型態分別爲 clojure.lang.BigInt 以及 java.math.BigDecimal。 99 | 100 | ```clojure 101 | (class 1N) 102 | ;; => clojure.lang.BigInt 103 | (class 2M) 104 | ;; => java.math.BigDecimal 105 | ``` 106 | 107 | 大整數的表示法爲在數字後加上大寫的 N;大浮點數則是在數字後加上大寫的 M。內建的 ```+```、```-```、```*```、```/```、```inc```、```dec``` 等運算元,作用在整數時如果超出 64 位元範圍 (overflow),會出現錯誤而拋出例外。如果想避免錯誤,讓 Clojure 把結果套用到大數上,則必須使用大數版的運算元,即在運算元後加上單引號 (‘): 108 | 109 | ```clojure 110 | (+ 9141592653589793238 9141592653589793238) 111 | ;; => ArithmeticException integer overflow 112 | (+' 9141592653589793238 9141592653589793238) 113 | ;; => 18283185307179586476N 114 | ``` 115 | 116 | ### 運算 117 | 118 | 基本的四則運算與其他主流程式語言一樣,需要注意的是 Clojure 採用前置表示法:運算元擺在小括號的第一個位置: 119 | 120 | ```clojure 121 | (+ 1 2) 122 | ;; => 3 123 | (- 1 3) 124 | ;; => -2 125 | (* 2 4) 126 | ;; => 8 127 | (/ 6 3) 128 | ;; => 2 129 | ``` 130 | 131 | 如果想要獲得整數除法運算的商,可以使用 ```quot``` 函式: 132 | 133 | ```clojure 134 | (quot 47 7) 135 | ;; => 6 136 | ``` 137 | 138 | 取得除法運算的餘,則使用 ```rem``` 函式: 139 | 140 | ```clojure 141 | (rem 47 7) 142 | ;; => 5 143 | ``` 144 | 145 | ## 字串與字符 146 | 147 | Clojure 的字串即是 Java 的 String 類型,表現方法也跟 Java 一樣用雙引號包住文字: 148 | 149 | ```clojure 150 | (class "foo") 151 | ;; => java.lang.String 152 | ``` 153 | 154 | 多行字串只要在需要的時候換行即可: 155 | 156 | ```clojure 157 | "This multi- 158 | line string" 159 | ;; => "This multi-\nline string" 160 | ``` 161 | 162 | 字符是字串的組成元素,使用方法爲將反斜線 (\\) 加到文字的前面,型態爲 java.lang.Character: 163 | 164 | ```clojure 165 | (class \j) 166 | ;; => java.lang.Character 167 | \j 168 | ;; => \j 169 | ``` 170 | 171 | 萬國碼 (Unicode) 以及八進位表示法也可以用在字符表示上: 172 | 173 | ```clojure 174 | \u00eb 175 | ;; => \ë 176 | \o44 177 | ;; => \$ 178 | ``` 179 | 180 | 請注意,八位元字符最前面加上的是反斜線 (\\) 與小寫字母 o (Octal),並不是數字 0。 181 | 182 | ## 布林 183 | 184 | 當運算式使用到流程控制時,需要使用布林型態來決定該往那個分支進行。Clojure 的布林型態有 ```true``` 以及 ```false``` 兩種。內部型態爲 Java 的 java.lang.Boolean: 185 | 186 | ```clojure 187 | (class true) 188 | ;; => java.lang.Boolean 189 | true 190 | ;; => true 191 | false 192 | ;; => false 193 | ``` 194 | 195 | Clojure 提供了一些函式用來判斷是否爲真: 196 | 197 | ```clojure 198 | (true? true) 199 | ;; => true 200 | (true? false) 201 | ;; => false 202 | ``` 203 | 204 | 在 Clojure 的命名習慣裡,會將一個返回真假值的函式,在名稱後面加上問號 (?)。這樣的函式稱做「述詞函式」 (Predicate)。 205 | 206 | Clojure 也提供了一些函式判斷是否爲假: 207 | 208 | ```clojure 209 | (false? false) 210 | ;; => true 211 | (false? true) 212 | ;; => false 213 | ``` 214 | 215 | 除了布林型態之外,```nil``` 用來表示不存在以及虛無,與 Java 中的 null 相同。當 ```nil``` 用在條件判斷時,```nil``` 被當作 ```false```: 216 | 217 | ```clojure 218 | (true? nil) 219 | ;; => false 220 | (false? nil) 221 | ;; => false 222 | (nil? nil) 223 | ;; => true 224 | ``` 225 | 226 | Clojure 使用 ```=``` 來判斷兩個事物是否相等,內部使用 Java 物件的 equals 方法來判斷: 227 | 228 | ```clojure 229 | (= 1 1) 230 | ;; => true 231 | (= "Hello" "HELLO") 232 | ;; => false 233 | (= "Hello" 1) 234 | ;; false 235 | ``` 236 | 237 | ## 符號 238 | 239 | 符號 (Symbol) 是個標識符 (Identifier),用來指向它所代表的值。對它求值時,會返回它指向的值。在 Clojure 中,所有非數字開頭的名稱都是一個個符號,分別代表數字、字串、集合或函式,每個符號都隸屬於一個命名空間。內部使用的型態爲 clojure.lang.Symbol: 240 | 241 | ```clojure 242 | (def username "Rich") 243 | ;; => #'user/username 244 | username 245 | ;; => "Rich" 246 | (class 'username) 247 | ;; => clojure.lang.Symbol 248 | ``` 249 | 250 | 在運算式中直接使用符號,Clojure 會嘗試對它求值,如果這個符號尚未指向任何資料,就會出現例外。所以需要在符號前面加上單引號 (‘),告訴 Clojure 這個符號不需要求值。 251 | 252 | ```clojure 253 | (class average) 254 | ;; => CompilerException java.lang.RuntimeException: Unable to resolve symbol: average in this context 255 | (class 'average) 256 | ;; => clojure.lang.Symbol 257 | ``` 258 | 259 | 你可以使用 ```symbol``` 函式創建一個符號: 260 | 261 | ```clojure 262 | (symbol "foo") 263 | ;; => foo 264 | (symbol "foo" "bar") 265 | ;; => foo/bar 266 | ``` 267 | 268 | ## 關鍵字 269 | 270 | 關鍵字 (Keyword) 跟符號一樣,是個標識符,但是跟符號不同的是,關鍵字並不指向任何資料,關鍵字被求值時,返回的仍是被求值的關鍵字,關鍵字只代表自己。命名時在名稱前加上冒號 (:),內部使用的資料型態爲 clojure.lang.Symbol: 271 | 272 | ```clojure 273 | (class :foo) 274 | ;; => clojure.lang.Symbol 275 | :foo 276 | ;; => :foo 277 | ``` 278 | 279 | 你可以使用 ```keyword``` 函式創建一個關鍵字: 280 | 281 | ```clojure 282 | (keyword "foo") 283 | ;; => :foo 284 | ``` 285 | 286 | 關鍵字常見的使用方法是跟映射 (Map) 搭配使用,作爲映射的索引鍵。 287 | 288 | 289 | ## 群集 290 | 291 | Clojure 的複合型別稱爲群集 (Collection),可以容納基本型別跟複合型別,所有的群集都是不可變 (Immutable) 以及持久存在 (Persistent)。 292 | 293 | Clojure 有四種群集型態,分別爲列表 (List)、向量 (Vector)、映射 (Map) 與集合 (Set),以下將對各個型態詳細介紹。 294 | 295 | ### 列表 296 | 297 | 列表是 Clojure 中最常見的資料結構,寫法是先寫下單引號 (‘),再使用左右小括號將其中的元素包裹起來: 298 | 299 | ```clojure 300 | '(1 2 3 4 5) 301 | ;; => (1 2 3 4 5) 302 | '(1 "foo" :bar "world") 303 | ;; => (1 “foo” :bar “world”) 304 | ``` 305 | 306 | 列表是由兩個部分組合而成,一個是列表的第一個元素,再來是除去第一個元素後剩下的元素,因此可以使用 ```first``` 函式取得列表的第一個元素,```rest``` 函式取得剩下來的元素: 307 | 308 | ```clojure 309 | (first '(:asimov :heinlein :bradbury :clarke :verne)) 310 | ;; => :asimov 311 | (rest '(:asimov :heinlein :bradbury :clarke :verne)) 312 | ;; => (:heinlein :bradbury :clarke :verne) 313 | ``` 314 | 315 | 若是想取得其後的各別單一元素,可以巢狀地使用 ```first``` 與 ```rest```: 316 | 317 | ```clojure 318 | (first (rest '(:asimov :heinlein :bradbury :clarke :verne))) 319 | ;; => :heinlein 320 | (first (rest (rest '(:asimov :heinlein :bradbury :clarke :verne)))) 321 | ;; => :bradbury 322 | (first (rest (rest (rest '(:asimov :heinlein :bradbury :clarke :verne))))) 323 | ;; => :clarke 324 | (first (rest (rest (rest (rest '(:asimov :heinlein :bradbury :clarke :verne)))))) 325 | ;; => :verne 326 | ``` 327 | 328 | 列表的最後一個元素是 nil,以表示列表已經到底: 329 | 330 | ```clojure 331 | (first (rest (rest '(1 2)))) 332 | ;; => nil 333 | ``` 334 | 335 | 除了使用實字 (Literal) 的方式寫下列表,還可以使用 ```list``` 函式創建列表: 336 | 337 | ```clojure 338 | (list :asimov :heinlein :bradbury :clarke :verne) 339 | ;; => (:asimov :heinlein :bradbury :clarke :verne) 340 | ``` 341 | 342 | 加入新元素到列表之中,可以使用 ```conj```: 343 | 344 | ```clojure 345 | (conj (list 1 2 3 4) 5) 346 | ;; => (5 1 2 3 4) 347 | ``` 348 | 349 | 也可以把列表當作堆疊來使用,使用 ```peek``` 取得列表頭部的第一個元素: 350 | 351 | ```clojure 352 | (peek (list 1 2 3 4)) 353 | ;; => 1 354 | ``` 355 | 356 | 使用 ```pop``` 取得尾部的其他元素: 357 | 358 | ```clojure 359 | (pop (list 1 2 3 4)) 360 | ;; => (2 3 4) 361 | ``` 362 | 363 | 不知道聰明的你是否注意到,列表與 Clojure 的程式碼表示方法完全一模一樣?有一個炫炮的名詞:同像性 (Homoiconicity),來稱呼這種既是程式、也是資料的表達方式。 364 | 365 | 具有同像性特色的程式語言,它表現出來的樣子已經跟編譯器使用的語法樹 (AST) 無異,亦即使用者寫出來的程式其實就已經是語法樹了。在其他語言中,語法樹資料結構被遮蓋在陰影之下,使用者如果想要新增語法,只能等待語言委員會經過漫長的投票表決之後,再實作出來。 366 | 367 | 但是具有同像性特色的程式語言,如果使用者覺得語法詞彙不敷使用,不必等待只要自己捲起袖子開工即可。至於怎麼新增自己的語法詞彙,將在之後講述巨集 (Macro) 的文章中介紹。 368 | 369 | ### 向量 370 | 371 | 使用列表時,如果想要取得特定位置的元素,必須從第一個元素開始往下找尋,而向量 (Vector) 則提供了類似列表的功能,但是可以從任意位置由索引直接取得。 372 | 373 | 列表使用中括號將元素包裹起來: 374 | 375 | ```clojure 376 | [1 2 3 4] 377 | ;; => [1 2 3 4] 378 | ``` 379 | 380 | ```first``` 與 ```rest``` 也可以作用在向量上: 381 | 382 | ```clojure 383 | (first [1 2 3 4]) 384 | ;; => 1 385 | (rest [1 2 3 4]) 386 | ;; => (2 3 4) 387 | ``` 388 | 389 | 將新的元素加入到向量中,仍然可以使用 ```conj```,只是加入的位置和列表不同: 390 | 391 | ```clojure 392 | (conj [1 2 3 4] 5) 393 | ;; => [1 2 3 4 5] 394 | ``` 395 | 396 | 由於列表在內部實作中,每個元素中只知道下一個元素的位置,插入新的元素最快速的方式便是放在頭部,而向量提供了更有效的存取方法,因此新元素可以安插至尾部。 397 | 398 | 使用 ```nth``` 搭配索引可以快速地取得其中的元素: 399 | 400 | ```clojure 401 | (nth [1 2 3 4 5] 0) 402 | ;; => 1 403 | (nth [1 2 3 4 5] 2) 404 | ;; => 3 405 | ``` 406 | 407 | 使用 ```count``` 可以取得列表或向量的元素總數: 408 | 409 | ```clojure 410 | (count [1 2 3 4 5]) 411 | ;; => 5 412 | ``` 413 | 414 | ### 映射 415 | 416 | 向量無法表現出資料對應的關係,Clojure 提供了映射 (Map) 可以將資料以索引鍵對應資料的方式存放。映射寫法以大括弧 ```{}``` 將索引鍵與資料成對擺放於其中: 417 | 418 | ```clojure 419 | {"a" 1 :b 2 :c 2} 420 | ;; => {"a" 1, :b 2, :c 2} 421 | ``` 422 | 423 | 也可以使用 ```hash-map``` 創建一個映射: 424 | 425 | ```clojure 426 | (hash-map "a" 1 :b 2 :c 3) 427 | ;; => {:c 3, "a" 1, :b 2} 428 | ``` 429 | 430 | 映射分爲有序與無序兩種,使用大括弧與 ```hash-map``` 創建的映射是無序的,所以順序可能會有不同。如果想建立有序的映射,可以使用 ```sorted-map``` 創建以索引鍵排序的映射: 431 | 432 | ```clojure 433 | (sorted-map :b 2 :c 3 :a 1) 434 | ;; => {:a 1, :b 2, :c 3} 435 | ``` 436 | 437 | 爲了更容易分辨,REPL 選擇以逗號 (,) 來分隔成對的元素,在 Clojure 中,逗號與空白是一樣的,並無二致。索引鍵必須是唯一的,不可重複出現。 438 | 439 | 你可以使用 ```get``` 函式並提供索引鍵,取得對應的資料: 440 | 441 | ```clojure 442 | (get {:a 1 :b 2 :c 2} :a) 443 | ;; => 1 444 | (get {:a 1 :b 2 :c 2} :c) 445 | ;; => 2 446 | (get {:a 1 :b 2 :c 2} :d) 447 | ;; => nil 448 | ``` 449 | 450 | 範例中示範了如果提供的索引鍵不存在於映射中,會回傳 nil。你也可以將映射當成函式呼叫,搭配索引鍵當作參數,則返回的結果是對應的值: 451 | 452 | ```clojure 453 | ({:a 1 :b 2 :c 3} :a) 454 | ;; => 1 455 | ``` 456 | 457 | 除此之外,關鍵字也可以當作函式來呼叫,以映射當作參數,則會傳回該關鍵字對應的值: 458 | 459 | ```clojure 460 | (:b {:a 1 :b 2 :c 3}) 461 | ;; => 2 462 | (:c {:a 1 :b 2 :c 3}) 463 | ;; => 3 464 | ``` 465 | 466 | 若是想修改映射的內容,可以使用 ```assoc``` 以及 ```dissoc``` 來新增或刪除內容,但是要注意的是,因爲在 Clojure 中群集都是不可變的,每次新增或刪除內容時都是產生新的映射。 467 | 468 | 使用 ```assoc``` 會傳回加入新內容的映射,第一個參數是舊的映射,第二以及第三個參數則是新增的索引鍵以及對應的值: 469 | 470 | ```clojure 471 | (assoc {:a 1 :b 2 :c 3} :d 4) 472 | ;; => {:a 1, :b 2, :c 3, :d 4} 473 | ``` 474 | 475 | ```dissoc``` 則是根據提供的索引鍵,傳回刪除了索引鍵與資料的新映射: 476 | 477 | ```clojure 478 | (dissoc {:a 1, :b 2, :c 3, :d 4} :c) 479 | ;; => {:a 1, :b 2, :d 4} 480 | ``` 481 | 482 | 以上的範例將索引鍵 ```:c``` 以及對應的資料刪去。 483 | 484 | ### 集合 485 | 486 | 最後一個要提到的群集是集合 (Set),集合中的資料必須唯一不重複。它的寫法是使用大括弧```{}``` 將資料包覆起來,並在最前面寫上井號 (#): 487 | 488 | ```clojure 489 | #{1 2 3 4 5} 490 | ;; => #{1 4 2 3 5} 491 | #{:asimov :heinlein :bradbury} 492 | ;; => #{:heinlein :asimov :bradbury} 493 | ``` 494 | 495 | 也可以使用 ```hash-set``` 函式建立一個集合: 496 | 497 | ```clojure 498 | (hash-set 1 2 3 4 5) 499 | ;; => #{1 4 3 2 5} 500 | (hash-set :asimov :heinlein :bradbury) 501 | ;; => #{:heinlein :asimov :bradbury} 502 | ``` 503 | 504 | 如果硬要塞入重複的資料,Clojure 會丟出例外強制停止: 505 | 506 | ```clojure 507 | #{1 2 3 4 5 2} 508 | ;; => IllegalArgumentException Duplicate key: 2 509 | ``` 510 | 511 | 集合跟映射一樣也分成無序和有序兩個版本,如果想建立有序的集合可以使用 ```sorted-set``` 函式創建集合: 512 | 513 | ```clojure 514 | (sorted-set 2 4 5 3 1) 515 | ;; => #{1 2 3 4 5} 516 | ``` 517 | 518 | clojure.set 這個命名空間 (Namespace) 中包含了可以操作集合的函式,想要使用 clojure.set 的函式,可以先執行以下運算式: 519 | 520 | ```clojure 521 | (use 'clojure.set) 522 | ;; => nil 523 | ``` 524 | 525 | 以上運算式將 clojure.set 命名空間載入到目前使用的命名空間中,可以開始使用 clojure.set 的所有符號 (命名空間將會在後續的章節中詳細介紹)。 526 | 527 | 其中 ```union``` 函式將會依據傳入的兩個集合,組合之後以集合傳回: 528 | 529 | ```clojure 530 | (union #{1 2 3} #{3 4 5}) 531 | ;; => #{1 4 3 2 5} 532 | ``` 533 | 534 | ```difference``` 函式會回傳一個新的集合,內容爲包含帶入的第一個集合,但是不包含第二個集合的內容: 535 | 536 | ```clojure 537 | (difference #{1 2 3} #{3 4 5}) 538 | ;; => #{1 2} 539 | ``` 540 | 541 | ```intersection``` 函式則會傳回兩個集合相同的元素: 542 | 543 | ```clojure 544 | (intersection #{1 2 3} #{3 4 5}) 545 | ;; => #{3} 546 | ``` 547 | 548 | ## 群集與序列 549 | 550 | 前面提到的列表、向量、映射與集合都是 Clojure 中的群集,群集並不是實際的資料結構,它只是一組抽象的介面或協定,只要符合這些介面就可以被稱爲群集。Clojure 提供了一些可以作用在群集的函式,符合協定的群集都可以使用這些函式。 551 | 552 | 可以作用在群集的函式,有先前提及的 ```count``` 函式可以返回群集內元素的個數以及 ```conj``` 函式將新的元素加入群集: 553 | 554 | ```clojure 555 | (count [1 2 3 4 5]) 556 | ;; => 5 557 | (count #{1 2 3 4 5}) 558 | ;; => 5 559 | 560 | (conj [1 2 3] 4) 561 | ;; => [1 2 3 4] 562 | (conj '(1 2 3) 4) 563 | ;; => (4 1 2 3) 564 | ``` 565 | 566 | ```=``` 函式判斷兩個群集是否相等、```empty``` 函式則是傳回與參數相同型態的空群集: 567 | 568 | ```clojure 569 | (= [1 2 3] [1 2 3]) 570 | ;; => true 571 | (= '(1 2 3) '(1 2)) 572 | ;; => false 573 | 574 | (empty [1 2]) 575 | ;; => [] 576 | (empty {:a 1 :b 2}) 577 | ;; => {} 578 | ``` 579 | 580 | 所有群集都支援 ```seq``` 函式,它可以將帶入的群集轉換成序列 (Sequence) 這種抽象介面。可以把序列 (Sequence) 看成是看待資料的方式,它必須是循序擺放就像是列表一樣。```seq``` 除了可以將 Clojure 中的群集轉換成序列之外,字串、Java 中的群集與陣列以及任何實作 java.util.Iterable 介面的類別也可以轉換: 581 | 582 | ```clojure 583 | (seq [1 2 3]) 584 | ;; => (1 2 3) 585 | (seq "Clojure") 586 | ;; => (\C \l \o \j \u \r \e) 587 | (seq {:a 2 :b 1}) 588 | ;; => ([:a 2] [:b 1]) 589 | ``` 590 | 591 | 序列的核心函式主要有三個:```first```、```rest``` 以及 ```cons```。```first``` 在之前提到過,取得序列的第一個元素: 592 | 593 | ```clojure 594 | (first (seq [1 2 3 4 5 6])) 595 | ;; => 1 596 | ``` 597 | 598 | ```rest``` 函式也在之前提到,傳回除了第一個元素之外的其他元素: 599 | 600 | ```clojure 601 | (rest (seq [1 2 3 4 5 6])) 602 | ;; => (2 3 4 5 6) 603 | ``` 604 | 605 | 而 ```cons``` 則是產生新序列的函式,它將第一個參數的新元素加入到第二個參數的序列中 (因爲不可變動的特性,實際上是產生新的序列): 606 | 607 | ```clojure 608 | (cons :a [:b :c :d]) 609 | ;; => (:a :b :c :d) 610 | (cons 0 '(1 2 3 4)) 611 | ;; => (0 1 2 3 4) 612 | ``` 613 | 614 | ```cons``` 總是將新的元素加入到序列的開頭位置。 615 | 616 | ## 回顧 617 | 618 | 從本篇文章中你更深刻地了解數字、字串、布林、符號與關鍵字等資料結構,還深入認識了四種群集:列表、向量、映射和集合;知道了群集與序列只是抽象化的介面,有許多函式可以拿來運用。 619 | 620 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 621 | -------------------------------------------------------------------------------- /03.md: -------------------------------------------------------------------------------- 1 | # 繫結與函式 2 | 3 | 函式是函數式程式設計的核心,雖然各個流派談及函數式程式設計,都有自己的定見和看法,但是不變的核心仍然是函式,函式必須是程式語言的第一級公民 (First-class citizen)。 4 | 5 | 身爲程式語言第一級公民的函式,必須可以當成參數傳遞給其它函式、可以被作爲函式的返回值、可以把函式繫結給某個名稱、以及可以在動態執行期產生函式。 6 | 7 | 函式的事暫且按下不表,先讓我們了解 Clojure 中如何將資料給定個名字。 8 | 9 | ## 繫結 10 | 11 | ### def 12 | 13 | 在之前的章節中我們不斷地在 REPL 中輸入資料,如果想要引用資料必須重新輸入才行。如果有一種方法,可以將資料賦予名字,需要使用的時候只要利用名字就可以參考,免去一再重複輸入的不便。 14 | 15 | 當然有! 16 | 17 | Clojure 提供了類似其它程式語言定義變數的功能,透過 ```def``` 創建一個符號 (Symbol) 連結到資料,之後只要使用這個符號,Clojure 就會尋找到對應的資料: 18 | 19 | ```clojure 20 | (def answer 42) 21 | ;; => #'user/answer 22 | answer 23 | ;; => 42 24 | ``` 25 | 26 | 透過使用 ```def``` 創建了符號之後,其實符號並不直接參考到實際資料,而是參考到 ```def``` 所創造的 Vars 物件,Vars 物件則存放了實際的資料。Vars 物件對應了其它程式語言變數的概念,但是不建議在多執行緒環境下使用。如果想要管理不同執行緒之間共同使用的狀態,可以參考後續狀態管理的章節。 27 | 28 | Vars 物件被創建時,會賦予它預設的命名空間,之後的運算式可以透過符號取得對應的資料,它是全域的資料繫結 (Binding)。 29 | 30 | ```clojure 31 | answer 32 | ;; => 42 33 | user/answer 34 | ;; => 42 35 | ``` 36 | 37 | 想要單純使用 Vars 物件,而不是它儲存的資料,可以使用 ```var```: 38 | 39 | ```clojure 40 | (var answer) 41 | ;; => #'user/answer 42 | ``` 43 | 44 | Clojure 提供了簡易的寫法,在名稱前加上井號 (#) 以及單引號 ('): 45 | 46 | ```clojure 47 | #'answer 48 | ;; => #'user/answer 49 | ``` 50 | ### let 51 | 52 | 除了使用 ```def``` 建立全域的資料繫結 (Binding) 之外,Clojure 還提供 ```let``` 讓我們建立區域的資料繫結。使用 ```let``` 建立的繫結只在創建的區域內可見,出了區域之後就船過水無痕: 53 | 54 | ```clojure 55 | (def y 5) 56 | ;; => #'user/y 57 | (let [x 1 58 | y (+ x 2)] 59 | y) 60 | ;; => 3 61 | y 62 | ;; => 5 63 | ``` 64 | 65 | ```let``` 運算式接受一個向量,成對地擺放了名字與資料的對應,之後的運算式便可以使用剛剛設定好的繫結,一旦離開 ```let``` 運算式,原本在運算式中的繫結就消失不見了。 66 | 67 | ## 函式 68 | 69 | 談完了全域與區域的繫結,現在回來談談函數式程式設計的核心:函式。以下將介紹如何在 Clojure 建立函式,以及函式的應用。 70 | 71 | ### 建立函式 72 | 73 | 在 Clojure 中建立函式最簡單的方式是透過 ```fn``` 這個特殊形式 (Special form),建立的函式沒有名字,稱作匿名函式 (Anonymous function)。以下示範接受兩個參數,返回兩個參數相加的函式: 74 | 75 | ```clojure 76 | (fn [x y] (+ x y)) 77 | ;; => #function[user/eval11037/fn--11038] 78 | ``` 79 | 80 | 以上範例示範了接受 x 與 y 兩個參數的函式,將兩個參數相加之後返回。 81 | 82 | ```fn``` 接受 ```let``` 風格的繫結方式,將參數寫在向量中指定名稱與順序,向量之後是函式的本體 (Body),函式的返回值爲函式本體最後一個運算式求得的值,不需明確指定返回值。 83 | 84 | 呼叫函式時,參數的擺放位置依據定義的順序依序擺放: 85 | 86 | ```clojure 87 | ((fn [x y] (+ x y)) 3 5) 88 | ;; => 8 89 | ``` 90 | 91 | Clojure 提供了簡單的表示法可以快速地建立匿名函式 (Anonymous function),只要在括號前加上井號即可: 92 | 93 | ```clojure 94 | (#(str "Hello World")) 95 | ;; => "Hello World" 96 | ``` 97 | 98 | 如果使用這種簡明表示法創建只接受一個參數的函式,可以在函式中使用百分比符號 (%) 表示參數: 99 | 100 | ```clojure 101 | (#(str "Hello " %) "Mike") 102 | ;; => "Hello Mike" 103 | ``` 104 | 105 | 若是兩個參數以上,則在百分比符號之後分別加上 1, 2, 3 等數字,表明參數的個別順序: 106 | 107 | ```clojure 108 | (#(str "Hello " %1 ", " %2 ) "Mike" "Andy") 109 | ;; => "Hello Mike, Andy" 110 | ``` 111 | 112 | 113 | 搭配 ```def``` 可以將一個匿名函式 (Anonymous function) 配上名字,之後只要以名稱便可以呼叫函式: 114 | 115 | ```clojure 116 | (def adder (fn [x y] (+ x y))) 117 | ;; => #'user/adder 118 | (adder 3 5) 119 | ;; => 8 120 | ``` 121 | 122 | 用 ```fn``` 搭配 ```def``` 雖然可以建立具有名稱的函式,但是太繁瑣了,Clojure 傾向提供簡潔方式解決問題。因此於 ```fn``` 與 ```def``` 的基礎上,建立了 ```defn``` 用來建立函式: 123 | 124 | ```clojure 125 | (defn adder [x y] (+ x y)) 126 | ;; => #'user/adder 127 | (adder 2 6) 128 | ;; => 8 129 | ``` 130 | 131 | #### 區域繫結 132 | 133 | 在函式本體之中,可以透過 ```let``` 建立只存在於函式之中的區域符號 (Symbol)。範例中區域符號 ```a``` 是 ```x```、```b``` 是 ```x``` 與 ```y``` 的和、```c``` 則是 ```y```,返回值是三個區域符號的值加總: 134 | 135 | ```clojure 136 | (defn adder [x y] 137 | (let [a x 138 | b (+ x y) 139 | c y] 140 | (+ a b c))) 141 | ;; => #'user/adder 142 | (adder 1 2) 143 | ;; => 6 144 | ``` 145 | 146 | #### 說明文字 147 | 148 | ```defn``` 內部以 ```def``` 建立了 Vars 物件,指向以 ```fn``` 產生的函式,除了函式的參數與本體之外,你還可以加上函式的說明文字 (Docstring),向使用者說明使用方法或設計理念: 149 | 150 | ```clojure 151 | (defn adder 152 | "Sum of two variables" 153 | [x y] 154 | (+ x y)) 155 | ;; => #'user/adder 156 | (doc adder) 157 | ;; => ------------------------- 158 | ;; => user/adder 159 | ;; => ([x y]) 160 | ;; => Sum of two variables 161 | ``` 162 | 163 | 說明文字 (Docstring) 加在名稱之後、參數之前,可以寫上函式的說明、參數的意義或是使用函式需要注意的地方。函式中的說明文字,內部使用 ```def``` 將文字加入 Vars 物件的詮釋資料中 (Metadata): 164 | 165 | ```clojure 166 | (def a "Simple value" 5) 167 | ;; => #'user/a 168 | (doc a) 169 | ;; => ------------------------- 170 | ;; => user/a 171 | ;; => Simple value 172 | ``` 173 | 174 | ### 多載 175 | 176 | Java 中將類別裡擁有數個同樣名字的方法 (Method),參數個數卻不同稱爲多載 (Overloading),Clojure 也支援這種設計方法,同一個函式根據參數個數的不同,可以有不同的運算式,也可以呼叫同一個函式下不同參數個數的運算式: 177 | 178 | ```clojure 179 | (defn adder 180 | ([x] (adder x 10)) 181 | ([x y] (+ x y))) 182 | ;; => #’user/adder 183 | (adder 3) 184 | ;; => 13 185 | (adder 3 2) 186 | ;; => 5 187 | ``` 188 | 189 | 以上範例示範以一個參數和以兩個參數呼叫函式,由於根據參數而分開不同實作,所以結果並不相同。其中一個參數的版本還呼叫了自己的兩個參數的版本。 190 | 191 | ### 不定長度參數 192 | 193 | 有時候在定義函式的時候並不清楚參數的個數,或是想要接受不定個數的參數,Clojure 提供方法在定義函式時,聲明接受的是不定長度的參數。 194 | 195 | 在函式定義的參數向量中,在參數名稱前加上 ```&``` 符號與空白,則之後的參數都會包裝至序列之中: 196 | 197 | ```clojure 198 | (defn str-all-numbers [x & rest] 199 | (apply str "Hi " rest)) 200 | ;; => #'user/str-all-numbers 201 | (str-all-numbers 0 1 2 3 4) 202 | ;; => “Hi 1234” 203 | (str-all-numbers 0) 204 | ;; => “Hi ” 205 | ``` 206 | 207 | 在這個範例中,第一個呼叫 ```str-all-numbers``` 函式時代入了五個參數,```x``` 爲第一個參數,其他參數則被裝進以 ```rest``` 命名的列表 (List) 中,由於只用到了後續的參數,所以第一個參數並不會被印出。 208 | 209 | 不定長度參數在 Clojure 中還具備了可選的屬性,亦即呼叫有不定長度參數的函式時,參數可以提供也可以不提供。第二次呼叫 ```str-all-numbers``` 函式時只給了第一個參數,後續的參數便不會被印出。 210 | 211 | 範例使用到的 ```apply``` 函式,第一個參數是函式,其後的參數將會被依序套用到第一個參數的函式中。 212 | 213 | 以下範例示範結合多載與不定長度參數的函式定義方法: 214 | 215 | ```clojure 216 | (defn overloading-variadic 217 | ([] 0) 218 | ([x] 1) 219 | ([x y] 2) 220 | ([x y & rest] "many arguments")) 221 | ;; => #'user/overloading-variadic 222 | (overloading-variadic) 223 | ;; => 0 224 | (overloading-variadic "one") 225 | ;; => 1 226 | (overloading-variadic "one" "two") 227 | ;; => 2 228 | (overloading-variadic "one" "two" "three") 229 | ;; => "many arguments" 230 | ``` 231 | 232 | ### 解構 233 | 234 | 這裡有一個函式,接受一個列表當作參數,參數的作用是將列表中的第二與第四個位置相加: 235 | 236 | ```clojure 237 | (defn useless-adder [lst] 238 | (let [x (first (rest lst)) 239 | y (first (rest (rest (rest lst))))] 240 | (+ x y))) 241 | ;; => #'user/useless-adder 242 | (useless-adder [1 3 5 7 9]) 243 | ;; => 10 244 | ``` 245 | 246 | 看到層層 ```first``` 與 ```rest``` 是不是看到頭暈了呢?Clojure 提供了簡便的語法,可以更快速地取得參數的內容,稱爲解構 (Destructuring)。 247 | 248 | #### 向量解構 249 | 250 | 向量解構可以使用在任何序列型的群集,例如列表、向量、字串或序列。如同前一個例子,使用向量解構取得第二與第三個元素,會變得非常簡單: 251 | 252 | ```clojure 253 | (let [[_ x _ y] [1 3 5 7 9]] 254 | (+ x y)) 255 | ;; => 10 256 | ``` 257 | 258 | 這裡把不需要理會的元素以底線 (_) 來表示,需要取得的元素則賦予名字,根據擺放的位置匹配適當的元素。 259 | 260 | 巢狀向量中的元素也可以匹配: 261 | 262 | ```clojure 263 | (let [[_ _ [x y]] [1 2 [3 4] 5]] 264 | (* x y)) 265 | ;; => 12 266 | ``` 267 | 268 | 可以在解構式最後加上 ```:as``` 繫結整個待解構的向量: 269 | 270 | ```clojure 271 | (let [[x y :as original] [1 2 3 4 5]] 272 | (conj original (+ x y))) 273 | ;; => [1 2 3 4 5 3] 274 | ``` 275 | 276 | 除了使用 ```:as``` 之外,還可以使用 ```&``` 來匹配其他未匹配的剩餘元素: 277 | 278 | ```clojure 279 | (let [[x & rest] [10 20 30 40 50]] 280 | rest) 281 | ;; => (20 30 40 50) 282 | ``` 283 | 284 | 當然也可以將 ```:as``` 與 ```&``` 兩個結合起來: 285 | 286 | ```clojure 287 | (let [[x & rest :as original] [2 4 6 8 10]] 288 | (println "x:" x ", rest:" rest ", original:" original)) 289 | ;; => x: 2 , rest: (4 6 8 10) , original: [2 4 6 8 10] 290 | ``` 291 | 292 | 以上的範例使用了 ```println``` 將資料輸出到螢幕,並加上換行。 293 | 294 | 有了解構之後,函式的參數就可以輕鬆地取得對應的內容: 295 | 296 | ```clojure 297 | (defn useful-adder [[_ x _ y]] 298 | (+ x y)) 299 | ;; => #'user/useful-adder 300 | (useful-adder [1 3 5 7 9]) 301 | ;; => 10 302 | ``` 303 | 304 | 跟一開始複雜的範例相比,是不是簡單很多呢。 305 | 306 | #### 映射解構 307 | 308 | 解構映射也跟解構向量一樣,根據擺放的位置與索引鍵,匹配對應的資料: 309 | 310 | ```clojure 311 | (def m {:a 5 :b 10 "c" 15}) 312 | ;; => #'user/m 313 | (let [{a :a b :b} m] 314 | (+ a b)) 315 | ;; => 15 316 | ``` 317 | 318 | ```a``` 匹配索引鍵 ```:a``` 對應的資料、```b``` 匹配索引鍵 ```:b``` 對應的資料。找不到對應的資料,會得到預設的 ```nil```,如果不想使用預設的 ```nil```,亦可以透過 ```:or``` 指定當某索引鍵找不到資料時,預設取得的資料: 319 | 320 | ```clojure 321 | (def m {:a 5 :b 10 "c" 15}) 322 | ;; => #'user/m 323 | (let [{a :a b :b d :d :or {d "OH"}} m] 324 | (println a b d)) 325 | ;; => 5 10 OH 326 | ``` 327 | 328 | 以上範例試圖取用索引鍵 ```:d``` 對應的資料,並在 ```:or``` 提供一個映射,指定了當找不到對應的資料時,應該選用的預設資料。 329 | 330 | 在向量解構時使用到的 ```:as``` 也可以使用在這裏,唯一不同的是不需要擺放在最後位置 (但建議還是放在最後): 331 | 332 | ```clojure 333 | (let [{a :a b :b :as whole} m] 334 | (println a b whole)) 335 | ;; => 5 10 {:a 5, :b 10, c 15} 336 | ``` 337 | 338 | 如果打算匹配的映射,其中的索引鍵都是由關鍵字組成的,Clojure 提供了 ```:keys``` 用來匹配映射中的關鍵字索引鍵。```:keys``` 後加上一個向量,其中寫下打算匹配的關鍵字名稱,匹配後就可以透過名稱取得資料: 339 | 340 | ```clojure 341 | (def m {:a 10 :b 20 :c 15}) 342 | ;; => #'user/m 343 | (let [{:keys [a b]} m] 344 | (println a b)) 345 | ;; => 10 20 346 | ``` 347 | 348 | 如果映射中的索引鍵都是使用字串則使用 ```:strs```、都是使用符號則用 ```:syms```: 349 | 350 | ```clojure 351 | (let [{:strs [a d]} {"a" "A", "b" "B", "c" "C", "d" "D"}] 352 | (println a d)) 353 | ;; => A D 354 | (let [{:syms [a d]} {'a "A", 'b "B", 'c "C", 'd "D"}] 355 | (println a d)) 356 | ;; => A D 357 | ``` 358 | 359 | 使用了映射解構的函式,就可以輕鬆地取出索引鍵代表的資料了: 360 | 361 | ```clojure 362 | (defn greet-user [{:keys [first-name last-name]}] 363 | (println "Welcome," first-name last-name)) 364 | ;; => #'user/greet-user 365 | (def catherine {:first-name "Catherine", :last-name "Chen", :age 40}) 366 | ;; => #'user/catherine 367 | (greet-user catherine) 368 | ;; => Welcome, Catherine Chen 369 | ``` 370 | 371 | ### 高階函式 372 | 373 | 之前提到,函式在 Clojure 中是一等公民,像資料一樣,可以當成參數傳遞給其它函式,或可以被當成返回值傳遞。而可以做到其中之一功能的函式便稱作高階函式 (Higher-order Function)。 374 | 375 | Clojure 之中有許多函式都可以接受函式當作參數,例如 ```map``` 接受一個函式以及群集當作參數,它會遍歷群集中的各個元素,把每個元素套用到當作參數的函式中,套用後的各個返回值再放到新的序列裡。這種功能稱作「映射」。 376 | 377 | 以下的例子示範了利用 ```map``` 函式,將向量中的各個字串,利用 ```clojure.string/lower-case``` 函式轉成小寫: 378 | 379 | ```clojure 380 | (map clojure.string/lower-case ["White" "Black" "Red"]) 381 | ;; => ("white" "black" "red") 382 | ``` 383 | 384 | 以上的範例相當於對每個元素呼叫 ```clojure.string/lower-case```: 385 | 386 | ```clojure 387 | [(clojure.string/lower-case "White") (clojure.string/lower-case "Black") (clojure.string/lower-case "Red")] 388 | ``` 389 | 390 | 除了「映射」之外還有「化約」功能的 ```reduce``` 函式。```reduce``` 如同 ```map``` 一樣接受函式與群集當作參數,它會遍歷群集中每個元素,套用到當作參數的函式。每次一個元素套用函式之後的結果,將會與下一個元素一起套用到當作參數的函式中。 391 | 392 | 以下範例示範如何使用 ```reduce``` 計算出群集中所有元素的和: 393 | 394 | ```clojure 395 | (reduce + [1 2 3 4 5]) 396 | ;; => 15 397 | ``` 398 | 399 | 以上的範例相當於先計算出 1 + 2 的結果,再將結果加上 3、加上 4、最後加上 5: 400 | 401 | ```clojure 402 | (+ (+ (+ (+ 1 2) 3) 4) 5) 403 | ;; => 15 404 | ``` 405 | 406 | 利用 ```filter``` 函式則可以依據當作參數的函式其中的條件,來決定新的序列中究竟要放上什麼元素: 407 | 408 | ```clojure 409 | (filter #(> % 5) [2 3 5 10 15]) 410 | ;; => (10 15) 411 | ``` 412 | 413 | ```filter``` 遍歷群集中的元素,將每個元素各別代入到 ```#(> % 5)``` 匿名函式中,匿名函式中判斷是否大於 5。只要函式返回值是真,```filter``` 便將元素保留,否則剔除。因此新的序列裡只留下大於 5 的元素。 414 | 415 | ```filter``` 接受的函式返回布林值,這種函式被稱爲「述詞函式」(Predicate),命名習慣上會在名稱後加上問號 (?),以表明它的返回值不是真便是假。```even?``` 函式如果接受到偶數則返回真,反之則否,以下範例將奇數剔除,只留下偶數: 416 | 417 | ```clojure 418 | (filter even? [2 3 4 5 6]) 419 | ;; => (2 4 6) 420 | ``` 421 | 422 | ```some``` 則是接受述詞函式與一個群集,遍歷群集中的元素並逐個丟給述詞函式,只要遇到元素讓述詞函式返回真,```some``` 則返回真,反之則返回 ```nil```: 423 | 424 | ```clojure 425 | (some #(> % 5) [1 3 5 7 9]) 426 | ;; => true 427 | (some nil? [1 3 5 7 9]) 428 | ;; => nil 429 | ``` 430 | 431 | 以上範例分別示範了檢查群集中是否有大於 5 的元素,以及是否有 nil 元素在其中。 432 | 433 | ```every?``` 函式接受一個述詞函式和群集,只有群集中的每個元素都讓述詞函式返回真,```every?``` 函式才會返回真,反之則否。以下範例示範群集中的各個元素是否皆爲偶數: 434 | 435 | ```clojure 436 | (every? even? [1 2 3 4 5]) 437 | ;; => false 438 | (every? even? [2 4 6 8 10]) 439 | ;; => true 440 | ``` 441 | 442 | #### 組合函式 443 | 444 | 高階函式的另一個特色是可以返回一個函式當作結果,Clojure 提供了一些函式協助將一群函式組合成另一個函式返回。其中 ```comp``` 接受一群函式作爲參數,並返回新的函式,由右至左地呼叫傳入的函式。以下範例示範以組合的方式實作將字串中的空白去除,並將第一個字母改成大寫: 445 | 446 | ```clojure 447 | (def cap-without-space (comp clojure.string/capitalize clojure.string/trim)) 448 | (cap-without-space " clojure ") 449 | ;; => "Clojure" 450 | ``` 451 | 452 | 或是定義一個取出序列中第四個元素的函式: 453 | 454 | ```clojure 455 | (def fourth (comp first rest rest rest)) 456 | (fourth [1 2 3 4 5]) 457 | ;; => 4 458 | ``` 459 | 460 | ```partial``` 函式則是建立一個缺少的函式,缺少的是剩下的參數,通常使用在剩下的參數並不清楚的時候。以下的範例使用了 ```partial``` 建立了會加 5 的函式,由於產生的函式尚未完備,必須等剩下的參數補齊才會產生結果: 461 | 462 | ```clojure 463 | (def plus5 (partial + 5)) 464 | (plus5 2) 465 | ;; => 7 466 | (plus5 10) 467 | ;; => 15 468 | ``` 469 | 470 | 最後要討論的是 ```complement``` 函式,這個函式接受一個返回值是布林的函式,返回它相反的布林值: 471 | 472 | ```clojure 473 | ((complement even?) 2) 474 | ;; => false 475 | ((complement true?) false) 476 | ;; => true 477 | ``` 478 | 479 | 除了使用 ```comp```、```partial```、```complement``` 生成新函式的函式之外,當然也可以寫自己的函式來生成新函式。以下的範例演示了一個生成函式的函式,它接受一個參數後返回一個函式,以此參數來相加後續代入的參數: 480 | 481 | ```clojure 482 | (defn adder [x] 483 | (fn [y] (+ x y))) 484 | (def adder5 (adder 5)) 485 | (adder5 3) 486 | ;; => 8 487 | (adder5 10) 488 | ;; => 15 489 | ``` 490 | 491 | 以上的範例除了示範了返回函式,返回的函式還將創建時帶入到父函式的參數記住,供以後使用。這種函式被稱作閉包 (Closure)。 492 | 493 | ### 講個祕訣 494 | 495 | 有個與函式相關的祕訣:向量、映射與集合也可以當作函式來使用: 496 | 497 | ```clojure 498 | ([1 3 5 7] 2) 499 | ;; => 2 500 | (#{1 2 3} 1) 501 | ;; => 1 502 | ({:a 1 :b 2 :c 3} :c) 503 | ;; => 3 504 | ``` 505 | 506 | 向量當成函式時,參數就是索引值;映射當成函式時,參數就是索引鍵;集合當成函式時,參數就是集合中的內容,當參數並不在集合中則回傳 ```nil```。 507 | 508 | 若是將它們與高階函式一起使用,就可以產生簡潔的應用。以下範例使用 ```remove``` 函式,第一個參數是述語函式,用集合來當作述語函式。範例中,集合的內容是不受歡迎的賓客名字,第二個參數則是賓客名單,運算之後產生去除不受歡迎的賓客名單: 509 | 510 | ```clojure 511 | (def banned #{"Steve" "Michael"}) 512 | (def guest-list ["Brian" "Josh" "Steve"]) 513 | (remove banned guest-list) 514 | ;; => ("Brian" "Josh") 515 | ``` 516 | 517 | 或是使用 ```map``` 函式將向量中的特定元素抽取出來: 518 | 519 | ```clojure 520 | (map [:a :b :c :d :e] #{0 3}) 521 | ;; => (:a :d) 522 | ``` 523 | 524 | ## 遞迴 525 | 526 | ### 一般遞迴 527 | 528 | 遞迴是函式透過不斷呼叫自己,將問題切割成數個細小問題逐個解決之後,把結果統整起來的問題解決方式。函數式程式設計語言透過遞迴達成迴圈可以做到的事。 529 | 530 | 如果想要用遞迴來解決問題,首先必須要先將問題切割成有限的小問題,再來則要確定解決最小問題的方法。只要完成這兩件事,問題便可以順利解決。 531 | 532 | 舉例來說階乘函數的定義是:```n! = n * (n - 1) * (n - 2) · · · 3 * 2 * 1```,可以把 n 的階乘看成 n 乘上 (n - 1) 的階乘,而 (n - 1) 的階乘則是 (n - 1) 乘上 (n - 2) 的階乘。因此計算階乘只要不斷計算下一個階乘的值,直到 1 爲止將它們全部相乘: 533 | 534 | ```clojure 535 | (defn factorial [n] 536 | (if (= n 1) 537 | 1 538 | (*' n (factorial (- n 1))))) 539 | (factorial 10) 540 | ;; => 3628800 541 | ``` 542 | 543 | 以上的範例中使用了會自動轉換成大數的乘法符號 ```*'```,因爲產生的結果可能會超過一般整數的大小。 544 | 545 | 雖然這樣的表現非常自然直覺,但是缺點是因爲不斷呼叫自己,每次呼叫函式時會配置記憶體,其中存放參數資訊與到時返回的資訊,在最後一個函式返回之前,記憶體都不會釋放收回。一旦遞迴的次數過多,就會用光記憶體而無法正常運行: 546 | 547 | ```clojure 548 | (factorial 10000) 549 | ;; => StackOverflowError 550 | ``` 551 | 552 | ### 尾遞迴 553 | 554 | 因此爲了解決一般遞迴會發生的記憶體不足的問題,可以使用尾遞迴 (Tail Recursion) 的方式解決。尾遞迴仍然是遞迴,但是將遞迴呼叫的位置擺放在函式的尾端,並且在遞迴呼叫函式之前,已完成呼叫之前必要的計算。以下是改成尾遞迴的範例: 555 | 556 | ```clojure 557 | (defn tail-factorial 558 | ([n] 559 | (tail-factorial 1 1 n)) 560 | ([product counter max-count] 561 | (if (> counter max-count) 562 | product 563 | (tail-factorial (*' counter product) 564 | (+ counter 1) 565 | max-count)))) 566 | (tail-factorial 10000) 567 | ;; => StackOverflowError 568 | ``` 569 | 570 | 在一些程式語言例如 Scheme 會將尾遞迴的函式進行效能改進,但是在 Clojure 寄宿的 JVM 中並不會對尾遞迴實行改進,因此建議的做法是改用 Clojure 提供的 loop/recur: 571 | 572 | ```clojure 573 | (defn recur-factorial [n] 574 | (loop [product 1 575 | counter 1 576 | max-count n] 577 | (if (> counter max-count) 578 | product 579 | (recur (*' counter product) 580 | (+ counter 1) 581 | max-count)))) 582 | (recur-factorial 10000) 583 | ;; => 28462596809170545189….0000N 584 | ``` 585 | 586 | ## 惰性序列 587 | 588 | 將運算或求值延遲到必要的時候才進行稱作惰性求值 (Lazy evaluation),Clojure 提供了惰性序列 (Lazy sequence) 將計算序列內容延遲到真正需要的時候。Lisp 家族中的 Scheme 程式語言提供了類似的功能稱爲流 (Stream),Haskell 程式語言則是全面支援惰性求值。 589 | 590 | 因爲惰性序列延遲計算的特色,可以用來表現無限的概念,例如無限列表、或是讀取非常龐大的資料,在必要的時候才讀取資料至記憶體、或是將 IO 讀取延遲到真正需要的時候。內部使用的型態爲 clojure.lang.LazySeq。 591 | 592 | ```clojure 593 | (class (take 10 (range))) 594 | ;; => clojure.lang.LazySeq 595 | ``` 596 | 597 | 創建一個惰性序列最簡單的方式是呼叫 ```range``` 函式,它會根據傳遞的參數創建漸進的惰性序列,搭配 ```take``` 函式後,可以依據需要的個數取得序列的內容: 598 | 599 | ```clojure 600 | (range 10) 601 | ;; => (0 1 2 3 4 5 6 7 8 9) 602 | (range 1 11) 603 | ;; => (1 2 3 4 5 6 7 8 9 10) 604 | (range 1 11 2) 605 | ;; => (1 3 5 7 9) 606 | (take 5 (range)) 607 | ;; => (0 1 2 3 4) 608 | ``` 609 | 610 | 請記得千萬不要在 REPL 中直接使用 ```range``` 函式,會迫使 REPL 一直爲了顯示序列的內容而不斷求值,造成 REPL 停滯不動: 611 | 612 | ```clojure 613 | ;; 危險!不要這樣做!! 614 | (range) 615 | ``` 616 | 617 | ```repeat``` 創建惰性序列,內容為不斷重複的參數: 618 | 619 | ```clojure 620 | (take 3 (repeat "Hello")) 621 | ;; => ("Hello" "Hello" "Hello") 622 | ``` 623 | 624 | ```iterate``` 函式接受兩個參數,第一個參數爲一個函式,這個函式將會不斷地被運算,求值後的結果成爲惰性序列的內容;第二個參數則爲初始值: 625 | 626 | ```clojure 627 | (take 5 (iterate #(+ % 0.5) 1)) 628 | ;; => (1 1.5 2.0 2.5 3.0) 629 | ``` 630 | 631 | 以上範例中,```iterate``` 第一個參數使用到了匿名函式 (Anonymous Function)。 632 | 633 | ```cycle``` 函式接受一個群集,群集的內容將會交錯反覆地作爲惰性序列的元素: 634 | 635 | ```clojure 636 | (take 3 (cycle ["ping" "pong"])) 637 | ;; => ("ping" "pong" "ping") 638 | (take 5 (cycle ["ping" "pong"])) 639 | ;; => ("ping" "pong" "ping" "pong" "ping") 640 | ``` 641 | 642 | ```map``` 與 ```filter``` 函式可以應用在惰性序列上,產生的結果也是惰性序列。以下範例示範了利用 ```filter``` 與 ```take``` 取得 0 到 100 中頭十個偶數: 643 | 644 | ```clojure 645 | (take 10 (filter even? (range 0 100))) 646 | ;; => (0 2 4 6 8 10 12 14 16 18) 647 | ``` 648 | 649 | 當利用內建的函式產生的惰性序列不符合需求時,還可以利用 ```lazy-seq``` 依據自己的需求打造惰性序列。以下利用 ```lazy-seq``` 與遞迴,建立惰性的費式序列: 650 | 651 | ```clojure 652 | (defn fib-seq 653 | "Returns a lazy sequence of Fibonacci numbers" 654 | ([] 655 | (fib-seq 0 1)) 656 | ([a b] 657 | (lazy-seq 658 | (cons b (fib-seq b (+ a b)))))) 659 | (take 10 (fib-seq)) 660 | ;; => (1 1 2 3 5 8 13 21 34 55) 661 | ``` 662 | 663 | 以上範例首先使用 ```lazy-seq``` 創建惰性序列,其中使用 ```cons``` 函式創建序列,序列的尾部遞迴地呼叫 ```fib-seq``` 繼續生成序列。 664 | 665 | ## 回顧 666 | 667 | 通過本篇文章,你知道了如何建立 Vars 物件儲存資料,也了解如何建立函式。還了解了函式的各方面特色,如多載以及不定引數等。知道了使用解構手法,可以更方便快速取得需要的資料;當然還有像堆積木一樣地任意組合函式。除此之外,還知道了遞迴以及可以表達無限概念的惰性序列。 668 | 669 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 670 | -------------------------------------------------------------------------------- /04.md: -------------------------------------------------------------------------------- 1 | # 流程控制 2 | 3 | 流程控制是枝幹、河流與道路,將如同樹葉、土地與城市一樣的函式連結起來,藉由流程控制,程式可以選擇行走的方向,前進後退、左右轉或是不斷反覆。 4 | 5 | 本篇文章將介紹 Clojure 中流程控制的方法,其中出現的運算式與函式大不相同,它們只在需要的時候才會被求值,而不像函式在呼叫之前,所以參數必須完成求值。 6 | 7 | ## 條件式 8 | 9 | ### if, if-not 10 | 11 | 首先要介紹的,也是在前面就提到過的 ```if``` 運算式。```if``` 運算式接收三個參數,第一個參數運算式只要求值結果爲真,則會對第二個參數運算式求值,否則會對第三個運算式求值,第三個運算式可以提供也可以不提供。Clojure 中除了 ```nil``` 與 ```false``` 之外都會視爲真: 12 | 13 | ```clojure 14 | (if 42 "answer") 15 | ;; => "answer" 16 | (if false "answer") 17 | ;; => nil 18 | (if "hi" "say hi" "say no") 19 | ;; => "say hi" 20 | (if true "it's true" "it's false") 21 | ;; => "it's true" 22 | (if false "it's true" "it's false") 23 | ;; => "it's false" 24 | ``` 25 | 26 | 如果條件測試爲假,卻沒有提供第三個參數運算式 (else 部分),結果會是 ```nil```。 27 | 28 | 由於 ```if``` 運算式中的第二與第三個參數,只能允許是一個運算式,如果需要放置兩個運算式以上的話則需要將多個運算式以 ```do``` 包覆起來,將會逐個求值,結果爲 ```do``` 中最後一個運算式求值的結果: 29 | 30 | ```clojure 31 | (if true 32 | (do 33 | (println "Success") 34 | "It's true") 35 | (do 36 | (println "Fail") 37 | "It's false")) 38 | ;; => Success 39 | ;; => "It's true" 40 | ``` 41 | 42 | ```if-not``` 是 ```if``` 的反面,如果 ```if-not``` 的條件測試爲真,則會對第三個參數運算式求值,否則會對第二個參數求值: 43 | 44 | ```clojure 45 | (if-not true "it's true" "it's false") 46 | ;; => "it's false" 47 | (if-not false "it's true" "it's false") 48 | ;; => "it's true" 49 | ``` 50 | ### when, when-not 51 | 52 | ```when``` 是少了 else 部分的 ```if``` 運算式,```when``` 接受一個測試函式,如果測試函式返回真,則會對 ```when``` 的本體運算式求值後返回其值,反之則回傳 ```nil```。 53 | 54 | ```clojure 55 | (when false "nothing") 56 | ;; => nil 57 | (when true "anything") 58 | => "anything" 59 | ``` 60 | 61 | 而 ```when-not``` 就是 ```when``` 的反面,如果接收的測試函式返回假,便對本體運算式求值後返回其值,反之則回傳 ```nil```。 62 | 63 | ```clojure 64 | (when-not (> 5 2) "Five") 65 | ;; => nil 66 | (when-not (> 2 5) "Two") 67 | ;; => "Two" 68 | ``` 69 | 70 | ### if-let, when-let 71 | 72 | 如果你想要將測試函式的返回值記起來,以便之後使用,可以用 ```let``` 搭配 ```if``` 達到此功能: 73 | 74 | ```clojure 75 | (let [is-small (< 5 100)] 76 | (if is-small 77 | "smaller" 78 | "bigger")) 79 | ;; => "smaller" 80 | ``` 81 | 82 | Clojure 提供了簡便的寫法將兩者整合在一起,有 ```if-let``` 與 ```when-let``` 兩個版本可用: 83 | 84 | ```clojure 85 | (if-let [is-smaller (< 5 100)] 86 | "smaller" 87 | "greater") 88 | ;; => "smaller" 89 | (when-let [is-greater (> 100 5)] 90 | "greater") 91 | ;; => "greater" 92 | ``` 93 | 94 | ### cond 95 | 96 | ```cond``` 運算式類似於其他語言中的 ```switch-case``` 或 ```if-elsif```,它接受一對對運算式,每對運算式都有條件式,以及當條件式成立時,待求值的運算式。它會根據每對運算式,照順序對各個條件式一一測試,只要有一條件式爲真,則求值對應的運算式,就不會再往下求值: 97 | 98 | ```clojure 99 | (let [x 1] 100 | (cond 101 | (> x 0) "greater" 102 | (= x 0) "zero" 103 | (< x 0) "smaller")) 104 | ;; => "greater" 105 | ``` 106 | 107 | 從以上範例可以看到,每個條件判斷式之後都跟着一個運算式,依序對條件判斷式求值,若爲真則對之後的運算式求值,而不再繼續。 108 | 109 | 你可以在 ```cond``` 的最後一對運算式中,將條件判斷式擺放非 ```nil``` 及 ```false``` 的值,當前面的條件判斷式都失敗時,便會執行最後一段運算式,用來當作其他條件都失敗的預設值: 110 | 111 | ```clojure 112 | (let [temperature 20] 113 | (cond 114 | (> temperature 30) "Hot" 115 | (< temperature 15) "Cold" 116 | :default "Normal")) 117 | ;; => "Normal" 118 | ``` 119 | 120 | ### case 121 | 122 | ```case``` 與 ```cond``` 非常類似,```case``` 會以第一個參數,與之後成對的運算式中的第一個運算式相比較,若相等則回傳之後的運算式求值的結果;若沒有任何一個相等,將丟出 IllegalArgumentException 例外: 123 | 124 | ```clojure 125 | (let [color "red"] 126 | (case color 127 | "red" "Rose" 128 | "white" "Paper" 129 | "Blue" "Sky")) 130 | ;; => "Rose" 131 | 132 | (let [capital "Canberra"] 133 | (case capital 134 | "Dublin" "Ireland" 135 | "Cairo" "Egypt" 136 | "Tokyo" "Japan")) 137 | ;; => IllegalArgumentException No matching clause: Canberra 138 | ``` 139 | 140 | ## 迭代 141 | 142 | 由於 Clojure 中的資料結構都是不可變 (Immutable),所以沒有主流程式語言的 for 迴圈,因爲 for 迴圈需要在每次迭代 (Iteration) 修改變數以達到迭代的功能。透過遞迴與函式也能夠做到迴圈的功能。 143 | 144 | ### doseq 145 | 146 | 如果想要遍歷序列,在每次取得序列中的元素時,就執行一次程式,可以使用 ```doseq```。```doseq``` 接受一個向量跟運算式本體,向量中以類似 ```let``` 方式命名一個符號,每次循環繫結序列中的元素: 147 | 148 | ```clojure 149 | (doseq [x [1 2 3]] 150 | (println x)) 151 | ;; => 1 152 | ;; => 2 153 | ;; => 3 154 | ;; => nil 155 | ``` 156 | 157 | 每次迭代時,符號 ```x``` 繫結了元素的內容。 158 | 159 | ```doseq``` 在遍歷元素時還可設定條件修飾子 (Modifier),決定何時對遍歷時的運算式求值、設定迭代的終止條件、以及在每次遍歷中繫結符號作爲使用: 160 | 161 | ```clojure 162 | (doseq [x (range 5) 163 | y [10 20 30] 164 | :let [z (* x y)] 165 | :when (odd? x)] 166 | (println x y z)) 167 | ;; => 1 10 10 168 | ;; => 1 20 20 169 | ;; => 1 30 30 170 | ;; => 3 10 30 171 | ;; => 3 20 60 172 | ;; => 3 30 90 173 | ;; => nil 174 | ``` 175 | 176 | 以上範例中,```x``` 爲 0 到 4,```y``` 爲 10 20 30,每次迭代還會設定符號 ```z``` 爲 ```x``` 與 ```y``` 相乘的值。迭代時當 ```x``` 爲奇數才會運行 ```(println x y z)```。 177 | 178 | 以下範例則示範使用 ```:while``` 關鍵字,設定迭代停止條件。當 ```y``` 大於或等於 30 時,迭代便終止 : 179 | 180 | ```clojure 181 | (doseq [x (range 99) 182 | :let [y (* x x)] 183 | :while (< y 30)] 184 | (println [x y])) 185 | ;; => [0 0] 186 | ;; => [1 1] 187 | ;; => [2 4] 188 | ;; => [3 9] 189 | ;; => [4 16] 190 | ;; => [5 25] 191 | ;; => nil 192 | ``` 193 | 194 | ### dotimes 195 | 196 | ```dotimes``` 與 ```doseq``` 類似,第一個參數的向量中包含符號與數字,數字代表迭代的次數,符號則繫結了每次迭代的次數。迭代的次數爲 0 到 (n - 1)。 197 | 198 | ```clojure 199 | (dotimes [x 3] 200 | (println x)) 201 | ;; => 0 202 | ;; => 1 203 | ;; => 2 204 | ;; => nil 205 | ``` 206 | 207 | ### while 208 | 209 | ```while``` 非常類似於在其他命令式 (Imperative) 程式語言的兄弟,例如 Ruby 或 Java。```while``` 接受一個測試運算式與本體運算式,當此運算式求值結果爲假時,才會終止對本體運算求值的循環。 210 | 211 | ```clojure 212 | (def x (atom 5)) 213 | (while (> @x 0) 214 | (do 215 | (println @x) 216 | (swap! x dec))) 217 | ;; => 5 218 | ;; => 4 219 | ;; => 3 220 | ;; => 2 221 | ;; => 1 222 | nil 223 | ``` 224 | 225 | 以上範例中的 ```x``` 爲 Clojure 引用型態 (Reference type) 中的原子 (Atom) 型態,使用 ```atom``` 與參數建立原子型態,內容爲 5。若是想要取用原子型態中的資料,須使用 ```@``` 或 ```deref``` 才可取用到內容。引用型態將會在後續文章詳細解說。 226 | 227 | 範例的內容爲每次迭代都會檢查 ```x``` 是否大於 0,若否則印出其值並將 ```x``` 指向的值遞減。 228 | 229 | ### loop/recur 230 | 231 | 以上提到的控制迭代循環的 ```doseq```、```dotimes``` 以及 ```while``` 都是利用 Clojure 迭代的基本元素 ```loop``` 與 ```recur``` 來達成。```loop``` 特殊形式 (Special form) 利用類似 ```let``` 語法來繫結循環時會用到的符號與資料的對應,之後包含了循環時運行的本體運算式,```recur``` 運算式則設立下次循環時使用到的新值。以下範例使用 ```loop``` 與 ```recur``` 示範倒數的功能: 232 | 233 | ```clojure 234 | (loop [n 5] ; 1 235 | (if (zero? n) 236 | n ; 2 237 | (do 238 | (println n) 239 | (recur (dec n))))) ; 3 240 | ;; => 5 241 | ;; => 4 242 | ;; => 3 243 | ;; => 2 244 | ;; => 1 245 | ;; => 0 246 | ``` 247 | 248 | 1. ```loop``` 在此建立了 ```n``` 的資料繫結,內容初始爲 5。 249 | 250 | 2. 此處設立了當到達終止條件 n 爲零時,將執行的運算式。```loop``` 的返回值即爲此運算式求值後的結果。 251 | 252 | 3. 最後在此處 ```recur``` 設立了下次循環時,```loop``` 已建立的符號 ```n``` 的新值,在這裡爲 n - 1。設定完成後,n 將會以新的值從 ```1``` 處重新開始循環。 253 | 254 | 由於 JVM 缺乏尾遞迴 (Tail Recursion) 的最佳化功能,以及遞迴時會消耗記憶體的問題,Clojure 中的使用慣例是利用 ```loop``` 與 ```recur``` 達成迭代的功能。 255 | 256 | ```recur``` 除了可以返回 ```loop``` 設立的遞迴點 (Recursion point) 之外,也可以返回由函式定義產生的遞迴點。以下範例使用 ```defn``` 與 ```recur``` 來實作倒數功能. 257 | 258 | ```clojure 259 | (defn countdown [x] 260 | (if (zero? x) 261 | x 262 | (do 263 | (println x) 264 | (recur (dec x))))) 265 | ;; => #'user/countdown 266 | (countdown 3) 267 | ;; => 3 268 | ;; => 2 269 | ;; => 1 270 | ;; => 0 271 | ``` 272 | 273 | ## 列表推導 274 | 275 | Clojure 中的 ```for``` 與一般程式語言的 ```for``` 不同,它利用一個群集作爲來源,運用運算式以及條件式產生新的群集,這稱作列表推導 (List comprehension)。它接受類似 ```let``` 繫結綁定的方式,在向量中以符號繫結群集,之後的本體運算式將陸續被代入群集中的元素,本體運算式每次求值的結果則放入新的群集中,運行完畢後返回新的群集。 276 | 277 | 以下範例示範由代入的向量,產生向量中元素以及它的倍數組成一對的新向量: 278 | 279 | ```clojure 280 | (for [x [1 2 3 4 5]] [x (* x x)]) 281 | ;; => ([1 1] [2 4] [3 9] [4 16] [5 25]) 282 | ``` 283 | 284 | 前面曾提到 ```doseq``` 運算式有幾個條件修飾子 (Modifier) 可以調整產生的結果,其實 ```doseq``` 是依靠 ```for``` 打造出來的,```doseq``` 的修飾詞都是繼承自 ```for```,三種條件修飾子都是 ```for``` 提供給 ```doseq```。讓我們來看看 ```for``` 使用條件修飾子的樣貌: 285 | 286 | ```clojure 287 | (for [x (range 5) 288 | y (range 5) 289 | :let [z (+ x y)] 290 | :when (.isProbablePrime (BigInteger/valueOf z) 5)] 291 | (list x y)) 292 | ;; => ((0 2) (0 3) (1 1) (1 2) (1 4) (2 0) (2 1) (2 3) (3 0) (3 2) (3 4) (4 1) (4 3)) 293 | ``` 294 | 295 | 以上範例中 ```x``` 與 ```y``` 分別代表內容包含 0 到 4 的列表,當兩個向量中的元素相加起來爲質數,便將兩元素放入新群集裡。範例中示範了如何使用 Java 靜態方法與呼叫 BigInteger 的執行個體方法。與 Java 的溝通將於後續的文章中詳細介紹。 296 | 297 | ## 穿引巨集 298 | 299 | Clojure 語言之中有非常多的巨集 (Macro),它擴充了語言基本提供的功能,帶來了易用與方便,在後續文章中將會有詳細的介紹。這裡先介紹的巨集,它爲程式碼帶來可讀性,稱爲穿引巨集 (Threading Macro)。 300 | 301 | ### 首位穿引 302 | 303 | 假設 Clojure 工程師年薪的 5 % 會拿來買書,其中的 30 % 則用來購買技術相關書籍,每年花在購買技術書籍的錢,可以用以下公式計算出來: 304 | 305 | ``` 306 | spending = ((salary * 0.05) * 0.3) * year 307 | ``` 308 | 309 | 改寫成 Clojure 程式碼後,計算十年花費了多少錢: 310 | 311 | ```clojure 312 | (defn spending [salary year] 313 | (* (* (* salary 0.05) 0.3) year)) 314 | (spending 10000 10) 315 | ;; => 1500.0 316 | ``` 317 | 318 | 看起來運作的不錯,但是計算花費的函式本體一層又一層的計算,真不容易閱讀。首位穿引巨集 (Thread-first macro) 正是解決這個問題的好方法。首位穿引巨集的寫法看起來就像是箭頭,由一個橫線 (-) 加上大於符號 (>) 組成,首位穿引巨集中的第一個參數運算式求值完成後,結果會傳遞給下一個參數運算式的第一個參數。以下是改用首位穿引巨集之後的結果: 319 | 320 | ```clojure 321 | (defn spending [salary year] 322 | (-> (* salary 0.05) 323 | (* 0.3) 324 | (* year))) 325 | (spending 10000 10) 326 | ;; => 1500.0 327 | ``` 328 | 329 | 一圖勝千言: 330 | 331 | ``` 332 | +-------------------+ 333 | | | 334 | | * salary 0.05 +-----------+ 335 | | | | 336 | +-------------------+ | 337 | +-----+---v----+-----+ 338 | | | | | 339 | | * | | 0.3 +-----------+ 340 | | | | | | 341 | +-----+--------+-----+ | 342 | +-----+---v---+------+ 343 | | | | | 344 | | * | | year | 345 | | | | | 346 | +-----+-------+------+ 347 | ``` 348 | 349 | 修改過後的程式碼變得更容易閱讀。前一個運算式的結果,接上下一個運算式的第一個參數的位置,一目瞭然。 350 | 351 | ### 末位穿引 352 | 353 | 首位穿引巨集是將結果放在下一個運算式的第一個參數,而末位穿引巨集 (Thread-last macro) 則是將結果放在下一個運算式的最後一個參數,寫法爲一個橫線 (-) 加上兩個大於符號 (>)。 354 | 355 | 修改前的運算式如下所示: 356 | 357 | ```clojure 358 | (take 5 (map #(+ % 2) (range 10))) 359 | ;; => (2 3 4 5 6) 360 | ``` 361 | 362 | 使用末位穿引巨集改寫之後: 363 | 364 | ```clojure 365 | (->> (range 10) 366 | (map #(+ % 2)) 367 | (take 5)) 368 | ;; => (2 3 4 5 6) 369 | ``` 370 | 371 | 一圖勝千言: 372 | 373 | ``` 374 | +-------------+ 375 | | | 376 | | range 10 +---------------------+ 377 | | | | 378 | +-------------+ | 379 | +-----+----------+--v--+ 380 | | | | | 381 | | map | #(+ % 2) | +---------------+ 382 | | | | | | 383 | +-----+----------+-----+ | 384 | +------+---+--v--+ 385 | | | | | 386 | | take | 5 | | 387 | | | | | 388 | +------+---+-----+ 389 | ``` 390 | 391 | ## 回顧 392 | 393 | 通過本篇文章,你了解了如何使用條件式來決定應該對哪個運算式求值,也了解到達成循環迭代功能的方法;還知道了強大的列表推導,可以生成列表。使用穿引巨集則可以讓程式易於閱讀和理解。 394 | 395 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 396 | -------------------------------------------------------------------------------- /05.md: -------------------------------------------------------------------------------- 1 | # 命名空間與專案 2 | 3 | > 我心裡一直都在暗暗設想,天堂應該是圖書館的模樣。 4 | > 5 | > — 波赫士《關於天賜的詩》 6 | 7 | 本篇文章將介紹組織程式碼的方法,包括以類似功能或屬性歸類的命名空間 (Namespace),和組織程式碼檔案的專案結構,還有如何使用其他的第三方函式庫,以及用來輸入程式碼的編輯器。 8 | 9 | 在開始之前,如果你正在使用 REPL,請按下 Ctrl-D 終止它,並輸入 ```lein repl``` 重啓新的 REPL。 10 | 11 | ## 命名空間 12 | 13 | 你會將同樣功能或用途的東西放在一起,例如筆、維修工具或是車子。在 Clojure 中,使用命名空間 (Namespace) 將類似的程式碼歸類組織起來,你可以依據功能、用途、階層或是你的心情將程式碼歸類。 14 | 15 | Clojure 會將目前的命名空間資訊,儲存在名爲 ```*ns*``` 的全域命名空間物件之中,它的內部型態爲 clojure.lang.Namespace。如果想知道目前的命名空間,可以使用 ```ns-name``` 套用在 ```*ns*``` 物件上。 16 | 17 | ```clojure 18 | (class *ns*) 19 | ;; => clojure.lang.Namespace 20 | (ns-name *ns*) 21 | ;; => user 22 | ``` 23 | 24 | 因爲在 REPL 中,預設的命名空間就是 ```user```,所以使用 ```ns-name``` 函式便返回 ```user```。值得注意的是,在使用習慣上命名全域物件會在名稱兩側加上星號 (*)。 25 | 26 | ### 創建 27 | 28 | #### in-ns 29 | 30 | 除了開啓 REPL 時自動建立的命名空間之外,也可以依據自己的需要創建命名空間。使用 ```in-ns``` 函式會嘗試切換到以參數符號爲名的命名空間,如果該命名空間不存在,則建立之,創建成功後便切換到該命名空間: 31 | 32 | ```clojure 33 | user=> (ns-name *ns*) 34 | ;; => user 35 | user=> (ns-name (in-ns 'foo)) 36 | ;; => foo 37 | foo=> (def x "bar") 38 | ;; => #'foo/x 39 | ``` 40 | 41 | 以上範例特別將提示符號前面的命名空間寫出來,說明命名空間經由函式建立並成功地切換到 ```foo``` 之中。在繼續往下之前,請先將命名空間切換回 ```user```,因爲 Clojure 核心函式只在該命名空間有載入: 42 | 43 | ```clojure 44 | foo=> (in-ns 'user) 45 | ;; => #namespace[user] 46 | user=> 47 | ``` 48 | 49 | #### create-ns 50 | 51 | 如果只想建立命名空間,可以使用 ```create-ns``` 建立之,若該命名空間已經存在則不做任何動作。創建之後,可以利用 ```in-ns``` 來切換至新建的命名空間: 52 | 53 | ```clojure 54 | user=> (create-ns 'inception) 55 | ;; => #namespace[inception] 56 | user=> (in-ns 'inception) 57 | ;; => #namespace[inception] 58 | inception=> 59 | ``` 60 | 61 | 繼續往下之前,請按下 Ctrl-D 終止 REPL 之後,再輸入 ```lein repl``` 重啓新的 REPL。 62 | 63 | ### 引用 64 | 65 | 切換至新的命名空間之後,所有以 ```def``` 建立的符號、Vars 物件、函式都會歸屬於新的命名空間。因此當你在某個命名空間建立了事物,若在另一個命名空間裡想要取用,卻沒有明確指定命名空間,就會發生錯誤: 66 | 67 | ```clojure 68 | user=> (def cobb "Leonardo DiCaprio") 69 | user=> (in-ns 'inception) 70 | inception=> cobb 71 | ;; => Exception: Unable to resolve symbol: cobb in this context 72 | ``` 73 | 74 | 若想要引用其他命名空間的物件,可以使用命名空間加上符號的方式,取用需要的物件。寫法爲先寫上命名空間,再加上斜線 (/),之後放上符號名稱即可: 75 | 76 | ```clojure 77 | inception=> user/cobb 78 | ;; => "Leonardo DiCaprio" 79 | ``` 80 | 81 | #### refer 82 | 83 | 如果不想使用全名方式的引用,可以使用 ```refer``` 函式將其它命名空間中所有公開的 Vars 物件,在目前的命名空間中建立對應,以後便不需要再明確指定命名空間: 84 | 85 | ```clojure 86 | inception=> (clojure.core/refer 'user) 87 | inception=> cobb 88 | ;; => "Leonardo DiCaprio" 89 | ``` 90 | 91 | 由於在新的命名空間中,REPL 並不會載入核心函式所在的 ```clojure.core``` 命名空間,所以使用 ```refer``` 函式必須以全名方式使用。 92 | 93 | ```refer``` 函式提供了三個修飾子,分別是 ```:exclude```、```:only```、```:rename```,用來指定哪些 Vars 物件不在此命名空間中建立對應,或只取用哪些 Vars 物件、以及將 Vars 物件在目前命名空間中建立不同名稱的對應: 94 | 95 | ```clojure 96 | (clojure.core/refer 'clojure.core 97 | :exclude '(+ - * /) 98 | :rename '{str fmt}) 99 | (+ 1 2) 100 | ;; => Unable to resolve symbol: + in this context 101 | (fmt "Wake up, " "Cobb") 102 | => "Wake up, Cobb" 103 | ``` 104 | 105 | 以上範例在目前的命名空間中,建立了 ```clojure.core``` 命名空間中的 Vars 物件對應,但是並不包含四則運算符號,並將 ```str``` 符號重新命名爲 ```fmt```。 106 | 107 | 繼續往下之前,請按下 Ctrl-D 終止 REPL 之後,再輸入 ```lein repl``` 重啓新的 REPL。 108 | 109 | #### require 110 | 111 | ```require``` 會負責將命名空間與相關資源載入,並編譯命名空間下的程式碼,但是不在目前的命名空間建立新的 Vars 物件對應。因此載入命名空間後,仍然必須寫明命名空間才可取用: 112 | 113 | ```clojure 114 | (require 'clojure.string) 115 | (clojure.string/join ", " ["Cobb" "Arthur" "Ariandne" "Eames"]) 116 | ;; => "Cobb, Arthur, Ariandne, Eames" 117 | ``` 118 | 119 | ```require``` 提供了修飾子 ```:as```,讓你將載入的命名空間以自己的需要重新命名: 120 | 121 | ```clojure 122 | (require '[clojure.string :as str]) 123 | (str/capitalize "mal") 124 | ;; => "Mal" 125 | ``` 126 | 127 | 若是打算一次載入多個命名空間,可以使用如下寫法: 128 | 129 | ```clojure 130 | (require 'clojure.string 'clojure.test) 131 | ``` 132 | 133 | 或是這樣寫: 134 | 135 | ```clojure 136 | (require '(clojure string test)) 137 | ``` 138 | 139 | 以上範例載入了 ```clojure.string``` 以及 ```clojure.test```。 140 | 141 | #### use 142 | 143 | ```use``` 與 ```require``` 類似,但是 ```use``` 載入欲使用的命名空間後,會呼叫 ```refer``` 在目前的命名空間建立對應,因此不需要使用全名。由於內部使用了 ```refer``` 函式,因此 ```refer``` 函式的修飾子也可以在 ```use``` 使用: 144 | 145 | ```clojure 146 | (use '[clojure.string :only [split]]) 147 | (split "Cobb, Arthur, Ariandne, Eames" #", ") 148 | ;; => ["Cobb" "Arthur" "Ariandne" "Eames"] 149 | ``` 150 | 151 | 以上範例展示了使用 ```clojure.string``` 中的 ```split``` 函式,以字串 ```", "``` 作爲分隔,將字串切割成四塊小字串。 152 | 153 | #### import 154 | 155 | 除了以 Clojure 寫成的程式碼,還可以使用 ```import``` 來載入 Java 套件 (Package) 類別。使用 ```import``` 載入套件中的類別之後,使用類別就不需要再寫上套件全名: 156 | 157 | ```clojure 158 | (java.util.Date.) 159 | ;; => #inst "2017-12-25T07:05:53.372-00:00" 160 | (import java.util.Date) 161 | (Date.) 162 | ;; => #inst "2017-12-25T07:06:19.038-00:00" 163 | ``` 164 | 165 | 在類別後加入點符號 (.) 是 Clojure 提供的簡化方法,用來簡化 ```new``` 函式創建類別,以上的範例等同如下: 166 | 167 | ```clojure 168 | (new Date) 169 | ;; => #inst "2017-12-25T07:09:13.378-00:00" 170 | ``` 171 | 172 | 繼續往下之前,請按下 Ctrl-D 終止 REPL 之後,再輸入 ```lein repl``` 重啓新的 REPL。 173 | 174 | ### 保護資訊 175 | 176 | 以上的函式都會引用到命名空間中的公開資訊,如果有些資訊想要隱藏不被使用,可以在使用 ```def``` 設立 Vars 物件時加上 ```private``` 詮釋資料 (Metadata): 177 | 178 | ```clojure 179 | user=> (def pub "It's public") 180 | user=> (def ^:private priv "It's private") 181 | user=> (in-ns 'foo) 182 | foo=> (clojure.core/refer 'user) 183 | foo=> pub 184 | ;; => "It's public" 185 | foo=> priv 186 | ;; => Unable to resolve symbol: priv in this context 187 | ``` 188 | 189 | 上面的範例雖然使用了 user 命名空間的 priv 物件,卻因爲在定義時宣告私有,因此無法正常取用。使用插入符號 (^) 會將詮釋資料添加至 Vars 物件。讓 Clojure 讀取器 (Reader) 採用不同處理方式的字元,被稱爲讀取器巨集 (Reader Macro)。 190 | 191 | Clojure 提供了更簡便的方式定義私有函式,便是使用 ```defn-``` 定義函式。繼續以下範例之前,請先按下 Ctrl-D 終止 REPL,再輸入 ```lein repl``` 開啓新的 REPL: 192 | 193 | ```clojure 194 | user=> (defn- greeting [name] (str "Hello, " name)) 195 | user=> (in-ns 'bar) 196 | bar=> (clojure.core/refer 'user) 197 | bar=> (greeting "Catherine") 198 | ;; => Unable to resolve symbol: greeting in this context 199 | ``` 200 | 201 | ### ns 巨集 202 | 203 | 在實際的專案中,其實並不常使用 ```refer```、```require``` 以及 ```use```,Clojure 提供了 ```ns``` 巨集,既具備了載入其他命名空間的功能,還可以建立新的命名空間: 204 | 205 | ```clojure 206 | (ns examples.ns 207 | (:use clojure.test) 208 | (:require [clojure.zip :as zip]) 209 | (:import java.util.Date)) 210 | ``` 211 | 212 | ```ns``` 巨集會試着建立第一個參數名稱指定的命名空間,並切換到該命名空間,之後的修飾子分別對應了 ```use```、```require``` 與 ```import``` 等功能。 213 | 214 | 實際專案中,檔案會在一開始使用 ```ns``` 巨集以建立該檔案隸屬的命名空間,並寫上欲載入的其他命名空間。 215 | 216 | ## 專案 217 | 218 | ### 專案結構 219 | 220 | 進入這個小節之前,請先把 REPL 終止 (按下 Ctrl-D),並在命令列下切換到你擺放 Clojure 專案的目錄下,如果沒有,在家目錄下建立 ```Projects``` 是個不錯的主意。 221 | 222 | 假設現在的新專案是爲漢堡店建立網站,首先切換到家目錄的 ```Projects``` 目錄下,使用 ```Leiningen``` 建立名爲 ```burger-shop``` 的專案: 223 | 224 | ```sh 225 | $ cd ~/Projects 226 | $ lein new app burger-shop 227 | ``` 228 | 229 | Leiningen 建立的專案 ```burger-shop``` 會長得像這樣: 230 | 231 | ``` 232 | . 233 | ├── CHANGELOG.md 234 | ├── LICENSE 235 | ├── README.md 236 | ├── doc 237 | │ └── intro.md 238 | ├── project.clj 239 | ├── resources 240 | ├── src 241 | │ └── burger_shop 242 | │ └── core.clj 243 | └── test 244 | └── burger_shop 245 | └── core_test.clj 246 | ``` 247 | 248 | ```project.clj``` 爲專案的配置描述文件,記載了專案的名稱、授權、使用到的套件以及編譯選項;```resources``` 目錄則用來擺放程式會使用到的資源檔案;```LICENSE``` 與 ```README.md``` 則分別是此專案的授權聲明,以及 Markdown 格式的說明檔。 249 | 250 | 應用程式的原始碼被擺放在 ```src``` 目錄下,```test``` 目錄下則放了用來測試應用程式的測試程式。Clojure 遵照 Java 對於套件的目錄命名規則,即是 ```x.y.z``` 套件將放在 ```x/y/z``` 的目錄結構中。 251 | 252 | Leiningen 爲新專案建立了 ```burger_shop``` 這個命名空間,並依照規則創建目錄結構。使用你的編輯器,將 ```src/burger_shop/core.clj``` 檔案打開,它應該像下面這樣: 253 | 254 | ```clojure 255 | (ns burger-shop.core 256 | (:gen-class)) 257 | 258 | (defn -main 259 | "I don't do a whole lot ... yet." 260 | [& args] 261 | (println "Hello, World!")) 262 | ``` 263 | 264 | Leiningen 爲這個檔案建立了 ```burger-shop.core``` 命名空間,並擺放在 ```burger_shop``` 目錄中的 ```core.clj``` 檔案,Clojure 程式檔案以 ```clj``` 爲副檔名。由於 Java 目錄命名不可有橫線符號 (-),因此使用底線符號 (_) 取代之。 265 | 266 | ### 使用第三方函式庫 267 | 268 | 除了自己寫的程式之外,實際專案還會使用別人已經開發好的函式庫,想要使用第三方函式庫,需要先以編輯器打開專案目錄下的 ```project.clj```: 269 | 270 | ```clojure 271 | (defproject burger-shop "0.1.0-SNAPSHOT" 272 | :description "FIXME: write description" 273 | :url "http://example.com/FIXME" 274 | :license {:name "Eclipse Public License" 275 | :url "http://www.eclipse.org/legal/epl-v10.html"} 276 | :dependencies [[org.clojure/clojure "1.8.0"]] 277 | :main ^:skip-aot burger-shop.core 278 | :target-path "target/%s" 279 | :profiles {:uberjar {:aot :all}}) 280 | ``` 281 | 282 | 若是想要使用 ```cheshire``` 函式庫,以獲得解析 json 的功能,可以在 Clojars 找到的[頁面](https://goo.gl/2nazpT)中看到資訊: 283 | 284 | ``` 285 | Leiningen/Boot 286 | [cheshire "5.8.0"] 287 | ``` 288 | 289 | 將中括號內的文字並包含中括號,寫上 ```:dependencies``` 所在的那一行並存檔: 290 | 291 | ```clojure 292 | (defproject burger-shop "0.1.0-SNAPSHOT" 293 | :description "FIXME: write description" 294 | :url "http://example.com/FIXME" 295 | :license {:name "Eclipse Public License" 296 | :url "http://www.eclipse.org/legal/epl-v10.html"} 297 | :dependencies [[org.clojure/clojure "1.8.0"] 298 | [cheshire "5.8.0"]] 299 | :main ^:skip-aot burger-shop.core 300 | :target-path "target/%s" 301 | :profiles {:uberjar {:aot :all}}) 302 | ``` 303 | 304 | 當我們運行或編譯專案時,便會下載 ```cheshire``` 第三方函式庫: 305 | 306 | ```clojure 307 | $ lein run 308 | Retrieving cheshire/cheshire/5.8.0/cheshire-5.8.0.pom from clojars 309 | ... 310 | Hello, World! 311 | ``` 312 | 313 | 以上範例的最後一行即是專案執行的結果。 314 | 315 | ## 編輯器 316 | 317 | 俗話說:工欲善其事,必先利其器。好的編輯器能夠讓你更輕鬆地輸入程式、容易地測試程式,或是提供有用的資訊修正錯誤。以下介紹開發 Clojure 時,較常爲人使用的編輯器。 318 | 319 | ### Light Table 320 | 321 | 使用 ClojureScript (一種寄宿在 JavaScript 的 Clojure 語言) 開發的 [Light Table](https://goo.gl/dnV8jW),曾在衆籌平台 Kickstarter 募資成功。以即時回饋爲訴求,使用者輸入運算式後,可以快速地看到程式求值的結果。 322 | 323 | ### Nightcode 324 | 325 | 爲 Clojure 與 ClojureScript 開發的 [Nightcode](https://goo.gl/X2tTYK),內建了 Leiningen 與 Boot,整合性的開發環境對初學者非常友好。 326 | 327 | ### Eclipse 328 | 329 | [Eclipse](https://goo.gl/sHtjoc) 作爲 Java 界知名的免費整合開發工具,除了用來開發 Java 程式語言之外,透過內建的擴充系統與豐富的外掛模組,也可以撰寫 Clojure 程式。目前與 [Counterclockwise](https://goo.gl/Yjo4iN) 套件搭配使用,提供便利的 Clojure 開發環境。 330 | 331 | ### IntelliJ IDEA 332 | 333 | 由來自捷克的軟體開發公司 JetBrains 開發的 [IntelliJ IDEA](https://goo.gl/v8kqoN) ,也是 Java 界知名的整合開發環境。建議使用 [Cursive](https://goo.gl/rQ9xLP) 套件,它提供了智慧括號輸入,以及 REPL 整合等相關功能。 334 | 335 | ### Vim 336 | 337 | Vim 作爲一個歷久彌新的編輯器,安裝 [Fireplace](https://goo.gl/cT4vw3) 便可以使用智慧輸入與 REPL 整合等功能。 338 | 339 | ### Emacs 340 | 341 | 除了作爲歷久彌新的編輯器,Emacs 還建立了以 LISP 爲操作語言的環境,有志者可以透過 Emacs Lisp 語言開發自己需要的功能,強大又富有彈性。經由套件 [CIDER](https://goo.gl/qqPY1e) 的協助之下,Emacs 將變成強大的 Clojure 程式開發環境。 342 | 343 | 以上編輯器根據使用難易度,由簡單到困難編排而成,讀者可以根據自己的需要選擇適合的編輯器。 344 | 345 | ## 回顧 346 | 347 | 經由本篇文章你學到了自行創建命名空間的方法,還知道了如何載入其它的命名空間;並且了解一般專案的目錄結構,和使用第三方函式庫的方法。除此之外,還知道了哪些編輯器可以更快速方便的開發 Clojure 程式。 348 | 349 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! -------------------------------------------------------------------------------- /06.md: -------------------------------------------------------------------------------- 1 | # 資料型別與協定 2 | 3 | > 計算機科學有兩大難題:快取失效,爲事物命名以及差一錯誤。 4 | > 5 | > — 菲爾•卡爾頓 6 | 7 | 我們以程式語言中的物件,數值與函式形塑真實世界,雖然 Clojure 提供了列表、向量、映射與集合等基礎型態供我們使用,還是會有力有未逮的時候。 8 | 9 | Clojure 除了基本型別之外,還提供了自行建立型別,以及擴充現有型態的功能,或許是因爲它知道,我們體內的物件導向火花尚未熄滅,仍然等待發光的時刻。 10 | 11 | 這次會介紹在 Clojure 當中如何自定型別,如何擴充已經存在的型別,還有如何使用更優雅的方式做到物件導向程式設計。 12 | 13 | ## 自定型別 14 | 15 | ### defrecord 16 | 17 | 使用 ```defrecord``` 巨集可以建立自己的型別,第一個參數是型別的名字,接着在向量中分別寫上此型別內各個屬性的名稱: 18 | 19 | ```clojure 20 | (defrecord User [name age]) 21 | ;; => user.User 22 | ``` 23 | 24 | ```defrecord```函式會根據名稱,動態地建立對應的 Java 類別,稱爲記錄類型 (Record),內部以類似映射的方式實作。你在參數向量中指定的屬性都可以公開取得,命名習慣則是跟 Java 中一樣,採取 ```CamelCase``` 的命名規則。以 ```defrecord``` 建立自定型別之後,以型別名稱後加上點 (.) 並接上各屬性的值,即可創建新型別: 25 | 26 | ```clojure 27 | (User. "Catherine" 40) 28 | ;; => #user.User{:name "Catherine", :age 40} 29 | ``` 30 | 31 | 取得以 ```defrecord``` 建立的新型別中屬性的方法爲,在屬性名稱前加上點 (.),再帶入型別的執行個體 (Instance) 即可: 32 | 33 | ```clojure 34 | (.name (User. "Catherine" 40)) 35 | ;; => "Catherine" 36 | (.age (User. "Catherine" 40)) 37 | ;; => 40 38 | ``` 39 | 40 | 由於記錄類型實作了映射所屬的關聯類型 (Associative type),因此可以在映射上使用的函式,也可以套用在記錄類型上。 41 | 42 | ```clojure 43 | (assoc (User. "Catherine" 40) :city "Taipei") 44 | ;; => #user.User{:name "Catherine", :age 40, :city "Taipei"} 45 | (dissoc (User. "Catherine" 40) :age) 46 | ;; => {:name "Catherine"} 47 | ``` 48 | 49 | 以上的範例首先使用 ```assoc``` 爲記錄類型添加新的屬性,再使用 ```dissoc``` 將屬性之一刪除。由於在執行階段,動態地將原有類型的屬性刪除,因此返回值不再是記錄類型,而是映射。 50 | 51 | 記錄類型除了自動生成建構子函式(就是類別名稱加上點符號的函式)之外,還自動生成了工廠函式 (Factory function),用以建構記錄類型: 52 | 53 | ```clojure 54 | (->User "Catherine" 40) 55 | ;; => #user.User{:name "Catherine", :age 40} 56 | ``` 57 | 58 | 另外還生成了可以將映射轉換成記錄類型的工廠函式,它接受一個映射,映射中包含了用於建構記錄類型的資訊: 59 | 60 | ```clojure 61 | (map->User {:name "Allen" :age 42}) 62 | ;; => #user.User{:name "Allen", :age 42} 63 | ``` 64 | 65 | ### deftype 66 | 67 | ```deftype``` 函式類似於 ```defrecord```,可以創建一個 Java 物件類型,並具有建構子函式: 68 | 69 | ```clojure 70 | (deftype Point [x y]) 71 | ;; => user.Point 72 | (.x (Point. 2 5)) 73 | ;; => 2 74 | (.y (Point. 2 5)) 75 | ;; => 5 76 | ``` 77 | 78 | 除此之外,便沒有與記錄類型相似的地方了。用 ```deftype``` 建立的類型既不能以映射的方式操作,也沒有工廠方法可供使用。 79 | 80 | ### reify 81 | 82 | 使用 ```defrecord``` 或是 ```deftype``` 創建具名的類別,如果臨時想要繼承某些類型,又因爲使用場所只在當前的環境,不需要大費周章創立具名類別,可以使用 ```reify``` 來建立匿名類型 (Anonymous type)。 83 | 84 | ```reify``` 接受協定或是介面的名字爲參數,當作欲實作的類型,接下來是該類型中打算實作的方法,```reify``` 可以同時繼承多個協定或是介面。 85 | 86 | 例如在 AWT/Swing 中,如果要接收來自各方的資訊,必須實作各種傾聽者 (Listener) 方法。可以使用 ```reify``` 來完成這個要求: 87 | 88 | ```clojure 89 | (reify 90 | java.awt.event.MouseListener 91 | (mousePressed [this e] 92 | (println "Mouse pressed"))) 93 | ``` 94 | 95 | 以上範例實作了滑鼠傾聽者介面中的 ```mousePressed``` 方法。```mousePressed``` 方法共有兩個參數,第一個參數爲發出此事件的執行實體,第二個參數則爲代表該滑鼠事件的執行實體。 96 | 97 | ## 協定 98 | 99 | Java 提供介面 (Interface) 用來定義共通的函式,由各型別實作共通的函式,根據各別的實作而有不同的功能。程式便只看見抽象的函式,而不依賴於實體類別。 100 | 101 | 在 Clojure 中可以使用 ```definterface``` 定義介面,與 Java 一樣: 102 | 103 | ```clojure 104 | (definterface IAnimal 105 | (eat [food]) 106 | (sleep [])) 107 | ;; => user.IAnimal 108 | ``` 109 | 110 | Clojure 還提供了類似介面的概念:協定。與介面類似,但是協定沒有實作部分,只有一組函式規則。介面一旦定義之後,便很難改動;協定則可以動態擴充,不必擔心牽一髮而動全身: 111 | 112 | ```clojure 113 | (defprotocol StackOps 114 | (stack-push [this thing]) 115 | (stack-pop [this])) 116 | ;; => StackOps 117 | ``` 118 | 119 | 以上範例使用 ```defprotocol``` 定義了一組堆疊的函式操作:可以往堆疊推入東西,也可以從堆疊中取出東西。 120 | 121 | 你可以使用 ```deftype```、```defrecord```、```reify``` 實作協定: 122 | 123 | ```clojure 124 | (deftype TypeStack [coll] 125 | StackOps 126 | (stack-push [_ thing] (println "Type push")) 127 | (stack-pop [_] (println "Type pop"))) 128 | ;; => user.TypeStack 129 | (defrecord RecStack [coll] 130 | StackOps 131 | (stack-push [_ thing] (println "Record push")) 132 | (stack-pop [_] (println "Record pop"))) 133 | ;; => user.RecordStack 134 | (reify StackOps 135 | (stack-push [_ thing] (println "Reify push")) 136 | (stack-pop [_] (println "Reify pop"))) 137 | ;; => #object[user$eval10525$reify__10526 0x43ca678f "user$eval10525$reify__10526@43ca678f"] 138 | ``` 139 | 140 | ## 擴充 141 | 142 | 雖然可以使用 ```deftype```、```defrecord``` 或 ```reify``` 實作介面或協定,但是缺點是必須在定義型別時就確認,Clojure 提供了在建立型別之後,仍然可以將型別或協定擴充的方式。 143 | 144 | ### extend 145 | 146 | 我們可以使用 ```extend``` 函式擴充已經定義好的型別: 147 | 148 | ```clojure 149 | (defrecord Rectangle [x y]) 150 | ;; => user.Rectangle 151 | (def rect (Rectangle. 5 5)) 152 | ;; => #'user/rect 153 | (defprotocol Shape 154 | (draw [this])) 155 | ;; => Shape 156 | (extend Rectangle 157 | Shape 158 | {:draw (fn [this] (println "Draw Rectangle:" (.x this) (.y this)))}) 159 | (draw rect) 160 | ;; => Draw Rectangle: 5 5 161 | ``` 162 | 163 | 範例中先創建 Rectangle 記錄類型,並以此記錄類型建立執行實體 (Instance) 後,定義了名爲 Shape 的協定,再讓 Rectangle 型態實作 Shape 協定。先在協定之前建立好的 Rectangle 記錄類型,便有了 Shape 協定的實作。 164 | 165 | 可以看到 ```extend``` 方法的寫法是,先寫上欲實作介面的類型名稱,再寫上欲實作的協定名稱,最後是加上映射,內容爲關鍵字與匿名函式,關鍵字名稱即是協定中函式的名稱。 166 | 167 | ### extend-type 168 | 169 | ```extend``` 雖然很神奇很方便,但是定義實作函式的地方太繁瑣了,你可以利用 ```extend-type``` 簡化定義方式: 170 | 171 | ```clojure 172 | (extend-type Rectangle 173 | Shape 174 | (draw [this] (println "Draw Rectangle still using extend-type:" 175 | (.x this) 176 | (.y this)))) 177 | (draw rect) 178 | ;; => Draw Rectangle using extend-type: 5 5 179 | ``` 180 | 181 | ### extend-protocol 182 | 183 | 還有 ```extend-protocol```,可以讓多個型別同時實作某個協定,而 ```extend-type``` 則是讓某個型別同時實作多個協定: 184 | 185 | ```clojure 186 | (extend-protocol AProtocol 187 | AType 188 | (method-from-AProtocol [this x] 189 | (;.. implementation of AType 190 | )) 191 | BType 192 | (method-from-AProtocol [this x] 193 | (;.. implementation of BType 194 | )) 195 | CType 196 | (method-from-AProtocol [this x] 197 | (;.. implementation of CType 198 | ))) 199 | ``` 200 | 201 | 以上只是示例,並無法實際在 REPL 執行 202 | 203 | ## 多重方法 204 | 205 | 由 ```defrecord``` 與 ```defprotocol``` 的介紹,我們已經看到了主流物件導向語言如 Java/C++ 支持多型 (Polymorphism) 的方式,就是根據型態的不同,決定該執行的函式。 206 | 207 | 主流的物件導向語言使用繼承建立階層,以繼承階層實現多型。Clojure 的多型並不一定要綁定在型別上。除了協定和記錄類型之外,Clojure 還提供了更靈活的多型設計方法,稱爲多重方法 (Multi-method)。 208 | 209 | 要使用多重方法達到多型,首先必須先使用 ```defmulti``` 巨集。```defmulti``` 包括函式名稱和一個分派函式 (Dispatch function),分派函式被調用後,返回值用來決定該使用哪個函式。 210 | 211 | 接下來使用 ```defmethod``` 定義多重函式。參數接受函式名稱、代表該函式應該被呼叫的分派值、和函式參數與函式本體。 212 | 213 | 以下範例使用多重方法,計算不同計酬方式員工的薪水。正職員工以月薪給付薪水,不管超過月平均工作時數與否,都是領月薪;而派遣員工則是以小時計酬,如果工作時數不滿 40 小時,便以時薪乘以工作時數給薪,如果超過 40 小時,則給薪方式爲 40 小時時薪,再加上超過 40 小時的工作時數乘以時薪再乘以 1.5 倍: 214 | 215 | ```clojure 216 | (defrecord Employee [type hours salary]) 217 | 218 | (defmulti earnings 219 | (fn [employee] (.type employee))) ; 1 220 | 221 | (defmethod earnings 222 | :salaried ; 2 223 | [employee] (.salary employee)) 224 | 225 | (defmethod earnings 226 | :hourly 227 | [employee] 228 | (let [hours (.hours employee) 229 | salary (.salary employee)] 230 | (if (< hours 40) 231 | (* hours salary) 232 | (+ (* 40 hours) 233 | (* (- hours 40) salary 1.5))))) 234 | 235 | (earnings (Employee. :salaried 70 30000)) ; 3 236 | ;; => 30000 237 | (earnings (Employee. :hourly 50 200)) 238 | ;; => 11800.0 239 | ``` 240 | 241 | 以上範例在步驟 ```1``` 的地方爲此多重方法的分派函式,此函式的返回值決定該執行哪個函式;步驟 ```2``` 則是分派值,當分派函式的返回值與這個值一樣,便執行此處的函式。第一個計算的是正職員工的薪水,再來是派遣員工的薪水。步驟 ```3``` 呼叫 ```earnings``` 函式的參數會先丟給分派函式求得分派值,再根據分派值選擇適當的函式。 242 | 243 | 由於多重方法中的分派函式可以是任何函式,因此可以有各種變化,不像主流物件導向語言只能依據類型與階層完成單一分派 (Single dispatch)。 244 | 245 | ## 反射 246 | 247 | Clojure 提供一些具有反射 (Reflective) 能力的函式,用來檢查或驗證型態與協定之間的關係。首先介紹的是 ```extends?``` 函式,參數接受協定和型別,結果爲該型別是否擴充提供的協定: 248 | 249 | ```clojure 250 | (defprotocol Vehicle (go [this])) 251 | 252 | (defrecord Car [] 253 | Vehicle 254 | (go [this] "Go car")) 255 | 256 | (extends? Vehicle Car) 257 | ;; => true 258 | ``` 259 | 260 | ```extenders``` 則是列出有哪些型別以 ```extend``` 相關的函式實作當作參數的協定: 261 | 262 | ```clojure 263 | (defrecord Motorcycle [color]) 264 | ;; => user.Motorcycle 265 | (defrecord Truck [color]) 266 | ;; => user.Truck 267 | (extend-protocol Vehicle 268 | Motorcycle 269 | (go [this] "Go motorcycle") 270 | Truck 271 | (go [this] "Go truck")) 272 | ;; => (user.Motorcycle user.Truck) 273 | (extenders Vehicle) 274 | ;; => (user.Motorcycle user.Truck) 275 | ``` 276 | 277 | ```satisfies?``` 接受協定與執行實體爲參數,如果執行實體以 ```extend``` 相關函式實作此協定則返回真,反之則否: 278 | 279 | ```clojure 280 | (satisfies? Vehicle (Truck. "red")) 281 | ;; => true 282 | (satisfies? Vehicle 123) 283 | ;; => false 284 | ``` 285 | 286 | ## 回顧 287 | 288 | 經由本篇文章你了解到如何在 Clojure 中自定型別,和建立相似於 Java 中介面的協定;也知道了擴充協定的方法,更了解了比物件導向的繼承式多型還強大的多重方法,並知道了一些反射方法,取得型別與協定之間的關係。 289 | 290 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 291 | 292 | -------------------------------------------------------------------------------- /07.md: -------------------------------------------------------------------------------- 1 | # 與 Java 共舞 2 | 3 | Clojure 寄生於 Java 之中,汲取它的養分並試圖解放它的繁重。Java 有優秀的即時編譯 (Just-in-time compilation) 功能、垃圾資源回收 (Garbage collection)、HotSpot 虛擬機器與傑出的位元組程式碼。寄宿在其上的語言,不需自行實現便可以擁有強大的武器爲後援。 4 | 5 | 既然寄宿在 Java 之上,不能只理解 Clojure,而對 Java 視而不見。了解 Java 是認識平台、優秀的生態圈與工具,讓程式邁向卓越的方法。 6 | 7 | 本篇文章會從 Clojure 方呼叫 Java,以及 Java 方呼叫 Clojure 兩方面,爲各位介紹如何與 Java 共同合作。 8 | 9 | ## 從 Clojure 呼叫 Java 10 | 11 | ### 載入 Java 套件庫與類別 12 | 13 | Clojure 中載入 Java 套件庫的方法在先前的文章也有提到過,就是使用 ```import``` 載入 Java 函式庫: 14 | 15 | ```clojure 16 | (import java.util.Date) 17 | (Date.) 18 | ;; => #inst "2017-12-31T15:51:54.105-00:00" 19 | ``` 20 | 21 | 以上載入 ```java.util.Date``` 套件到目前的命名空間後,就可以順利使用 Date 類別。你也可以同時載入同個套件內的不同類別: 22 | 23 | ```clojure 24 | (import java.util.Date java.util.Calendar) 25 | ;; => java.uti.Calendar 26 | ``` 27 | 28 | 也可以使用下面範例載入同個套件下的類別: 29 | 30 | ```clojure 31 | (import '(java.util Date Calendar) 32 | '(java.net URI ServerSocket)) 33 | ;; => java.net.ServerSocket 34 | ``` 35 | 36 | ### 創建執行個體 37 | 38 | Clojure 使用 ```new``` 特殊形式 (Special form) 來創建 Java 類別的執行個體 (Instance),第一個參數是類別名稱,接著是該類別建構式的各個參數,返回值爲該類別的執行個體: 39 | 40 | ```clojure 41 | (def date (new java.util.Date)) 42 | date 43 | ;; => #inst "2017-12-31T17:10:35.227-00:00" 44 | ``` 45 | 46 | Clojure 提供了語法糖 (Syntactic Sugar),用來簡化頻繁輸入 ```new```,只要在類別名稱之後加上點 (.) 即可: 47 | 48 | ```clojure 49 | (String. "Hello") 50 | ;; => "Hello" 51 | ``` 52 | 53 | ### 存取成員 54 | 55 | 由於 Clojure 中的字串即是 Java 的字串類別 java.util.String,可以在 java.util.String 類別上使用的方法 (Method),都可以在 Clojure 字串使用,例如將文字改成大寫與小寫: 56 | 57 | ```clojure 58 | (. "Hello World" toUpperCase) 59 | ;; => “HELLO WORLD” 60 | (. "Hello World" toLowerCase) 61 | ;; => “hello world” 62 | ``` 63 | 64 | 使用方式就是在列表中的第一個位置放上點 (.),依序再放上執行個體 (Instance) 以及方法的名稱,如果還有參數則再依序放上方法的參數: 65 | 66 | ```clojure 67 | (. "Hello World" indexOf "ello") 68 | ;; => 1 69 | ``` 70 | 71 | Clojure 也爲此提供了語法糖,只要將方法名稱放在列表的第一個位置,並在方法名稱之前加上點 (.),之後列表的位置依序放入執行個體以及方法的參數即可: 72 | 73 | ```clojure 74 | (.toUpperCase "Hello World") 75 | ;; => “HELLO WORLD” 76 | (.indexOf "Hello World" "ello") 77 | ;; => 1 78 | ``` 79 | 80 | 靜態方法 (Static method) 以及靜態欄位 (Static field) 可以使用如上用點的方式呼叫,也可以使用語法糖 Class/Method 的方式: 81 | 82 | ```clojure 83 | (. java.lang.Math PI) 84 | ;; => 3.141592653589793 85 | java.lang.Math/PI 86 | ;; => 3.141592653589793 87 | ``` 88 | 89 | 還有可以簡化層層內嵌運算式的特殊形式,即是在運算式的第一個位置,放上兩個點符號 (.),用來將以下的範例簡化: 90 | 91 | ```clojure 92 | (. (. (. " Hello " trim) toUpperCase) length) 93 | ;; => 5 94 | ``` 95 | 96 | 以上範例先將字串前後空白去除,再將字串轉成大寫,之後算出字串的長度。最裡頭的運算式求值之後取得新的執行個體,再傳遞給下一個運算式求值,Clojure 則提供了兩個點符號的特殊形式,將內嵌多層的運算式變得平坦,更易寫、易讀和易於理解: 97 | 98 | ```clojure 99 | (.. " Hello" trim toUpperCase length) 100 | ;; => 5 101 | ``` 102 | 103 | ### 匿名類型 104 | 105 | 在前一章提到過的 ```reify``` 巨集,能夠用來繼承父類別,產生匿名類別: 106 | 107 | ```clojure 108 | (.listFiles (java.io.File. ".") 109 | (reify java.io.FileFilter 110 | (accept [this f] 111 | (.isDirectory f)))) 112 | ``` 113 | 114 | 以上範例使用 ```java.io.File``` 其中的 ```listFiles``` 方法,它接受一個實作 ```FileFilter``` 的執行個體,透過此執行個體的 ```accept``` 方法來決定要保留哪些檔案。範例中使用 ```reify``` 實作了 ```FileFilter``` 中的 ```accept``` 方法。 115 | 116 | 除了 ```reify``` 之外,```proxy``` 也提供產生匿名類別的功能,但是 ```reify``` 只能用來實作協議或是介面,無法用來擴充實際的型態;當你需要覆寫父類別的方法時,只能用 ```proxy``` 來達成。 117 | 118 | ```proxy``` 接受的第一個參數是向量,其中包含了欲實作繼承的類別或介面的名稱,第二個參數爲內含建構式參數的向量,接下來則是覆寫的函式,大略像以下的形式: 119 | 120 | ```clojure 121 | (proxy [class-or-interface-name] [constructor-parameters] 122 | (method-name-1 [method-parameters] 123 | method-body) 124 | (method-name-2 [method-parameters] 125 | method-body) 126 | ``` 127 | 128 | 由於 ```reify``` 以及 ```proxy``` 的函式爲閉包,可以訪問建立此閉包環境中的資訊,因此可以達成類似於執行個體屬性的功能: 129 | 130 | ```clojure 131 | (str (let [f "foo"] 132 | (proxy [Object] [] 133 | (toString [] f)))) 134 | ``` 135 | 136 | 值得注意的是,使用 ```proxy``` 建立的匿名類別中的函式,不需要在參數中有明確指定 ```this``` 指向該執行個體,```proxy``` 會隱式地建立 ```this``` 代表到時建立的執行個體;使用 ```reify```、```deftype```、```defrecord``` 則需要明確指定 ```this``` 參數。 137 | 138 | ### 具名類型 139 | 140 | ```reify``` 與 ```proxy``` 讓我們在動態時期建立匿名類型,然而也會有需要提供靜態具名類型的時候,尤其是 Clojure 端打算提供給 Java 端功能的時候。 141 | 產生具名類型的方式是透過 ```gen-class``` 巨集,它提供了許多修飾子來調整產生出來的類型相關屬性,除了指定類型名稱之外,其他都不是必要提供的。 142 | 143 | ```gen-class``` 巨集會根據所給予的資訊產生對應的 Java 類別檔案 (.class),以下介紹 ```gen-class``` 以及一些修飾子: 144 | 145 | ```clojure 146 | (gen-class 147 | :name tw.embracing-clojure.example ;; 類別名稱 148 | :extends com.example.baseclass ;; 父類別名稱 149 | :implements [com.example.IFace] ;; 欲實作的介面 150 | :constructors {[String] [String]} ;; 建構式的返回值型態與參數型態 151 | :init initialize ;; 指定當作建構式的函式 152 | :state state ;; 指定類別中的公有最終屬性 153 | :methods [[doSomething [Byte] String] ;; 類別的方法 154 | [show [] String]] 155 | ) 156 | (defn init [a b] 157 | [[(str a b)] {ref {}}]) 158 | (defn doSomething [this b] 159 | "do something") 160 | (defn show [this] 161 | "show") 162 | ``` 163 | 164 | - :name 165 | 166 | 定義了欲產生的類別名稱,此處不可缺少。 167 | 168 | - :extends 169 | 170 | 欲擴充的父類別名稱。 171 | 172 | - :implements 173 | 174 | 如果有欲實作的介面,則寫於此處的向量之中。 175 | 176 | - :constructors 177 | 178 | 該類別建構式的返回值與參數的型態寫於此處。寫法爲以一個映射中包含向量爲索引鍵,以及內容值爲向量的每對資料,其中索引鍵向量中寫下返回值的型態,內容向量則寫下各參數的型態。 179 | 180 | - :init 181 | 182 | 指定用來當作建構式的函式名稱,該函式必須返回一個向量,該向量包含兩個元素,其中一個是傳給父類別建構式各參數值的向量,以及代表狀態的值,該狀態值爲原子 (Atom) 型態。 183 | 184 | - :state 185 | 186 | 在此處指定的名稱,將會在產生的類別中建立一個同名的執行個體屬性,爲不可變動 (final)。它的值必須要在 ```init``` 函式中指定。 187 | 188 | - :methods 189 | 190 | 此處指名了產生的類別中新增的方法,不需要將欲覆寫的父類別方法寫在這。 191 | 192 | 除了這裡介紹的修飾子之外,沒有提到的部分有興趣的讀者可以參考[官方文件](https://goo.gl/xahR9u),或在 REPL 輸入 ```(doc gen-class)``` 即可取得詳細資訊。 193 | 194 | 之前的章節中提到過的 ```ns``` 巨集,除了可以建立命名空間之外,也具備產生具名類別的功能。其功能與提供的修飾子都跟 ```gen-class``` 一樣: 195 | 196 | ```clojure 197 | (ns com.example.clojure 198 | (:gen-class 199 | :methods [[show [] void]])) 200 | ``` 201 | 202 | 在 ```ns``` 巨集裡的 ```gen-class``` 若沒有指定 ```:name```,則使用該命名空間當作類別名稱。 203 | 204 | ### Java 陣列 205 | 206 | 在 Java 的方法中,有可能會遇到參數需要傳遞物件的陣列,或是該方法爲不定引數,此時就需要一些與 Java 陣列有關的協作函式,可以創建或是修改陣列。 207 | 208 | #### 創建陣列 209 | 210 | 你可以使用 ```into-array``` 將序列轉成陣列: 211 | 212 | ```clojure 213 | (into-array [\x \y \z]) 214 | ;; => #object["[Ljava.lang.Character;" 0x1a8aca92 "[Ljava.lang.Character;@1a8aca92"] 215 | ``` 216 | 217 | 也可以使用 ```make-array``` 函式,明確指定該陣列的型態與容量: 218 | 219 | ```clojure 220 | (make-array Integer/TYPE 10) 221 | ;; => #object["[I" 0x44bcf69d "[I@44bcf69d"] 222 | ``` 223 | 224 | 或是可以使用下列明確寫明型態的陣列創建函式: 225 | 226 | - boolean-array 227 | 228 | - byte-array 229 | 230 | - char-array 231 | 232 | - double-array 233 | 234 | - float-array 235 | 236 | - int-array 237 | 238 | - long-array 239 | 240 | - object-array 241 | 242 | - short-array 243 | 244 | ```clojure 245 | (long-array 10) 246 | ;; => #object["[J" 0x44e25f4d "[J@44e25f4d"] 247 | (char-array 5) 248 | ;; => #object["[C" 0x176b9225 "[C@176b9225"] 249 | (float-array 15) 250 | ;; => #object["[F" 0x19b993c9 "[F@19b993c9"] 251 | ``` 252 | 253 | ```#object["[J" 0x44e25f4d "[J@44e25f4d"]``` 代表是個原生 (Primitive) 的 long 型態陣列、```#object["[C" 0x176b9225 "[C@176b9225"]``` 則是原生的 char 型態陣列,而 ```#object["[F" 0x19b993c9 "[F@19b993c9"]``` 則是原生的 float 型態陣列。 254 | 255 | #### 存取陣列 256 | 257 | 存取 Java 陣列使用 ```aget``` 與 ```aset``` 兩個函式: 258 | 259 | ```clojure 260 | (def my-array (into-array ["a" "b" "c" "d"])) 261 | (aget my-array 2) 262 | ;; => "c" 263 | (aset my-array 2 "C") 264 | ;; => "C" 265 | ``` 266 | 267 | 參數分別爲陣列的索引值,與打算設定的新值。 268 | 269 | ### 有用的工具函式 270 | 271 | #### doto 272 | 273 | ```doto``` 巨集將第一個參數傳遞給其後運算式當作第一個參數。它將以下層疊的運算式: 274 | 275 | ```clojure 276 | (let [array (java.util.ArrayList.)] 277 | (.add array 11) 278 | (.add array 3) 279 | (.add array 7) 280 | array) 281 | ;; => [11 3 7] 282 | ``` 283 | 284 | 轉換成: 285 | 286 | ```clojure 287 | (doto (java.util.ArrayList.) 288 | (.add 11) 289 | (.add 3) 290 | (.add 7)) 291 | ;; => [11 3 7] 292 | ``` 293 | 294 | 使用 ```doto``` 簡化了許多繁瑣的步驟。 295 | 296 | #### memfn 297 | 298 | 因爲 Java 類型的方法無法在 Clojure 當作一級函式,所以與 ```map``` 等函式配合時,需要多一層匿名函式包裝: 299 | 300 | ```clojure 301 | (map #(.length %) ["Happy" "New" "Year"]) 302 | ;; => (5 3 4) 303 | ``` 304 | 305 | 使用 ```memfn``` 可以將執行個體的方法轉換成 Clojure 中的函式,與高階函式搭配使用: 306 | 307 | ```clojure 308 | (map (memfn length) ["Happy" "New" "Year"]) 309 | ;; => (5 3 4) 310 | ``` 311 | 312 | ### 處理例外 313 | 314 | 例外是程式運行中發生非預期的問題時,產生出來的類型。根據產生的例外,做適當的處理或善後,是健壯穩定的程式必要的條件。Clojure 使用在 Java 中處理例外的關鍵字 ```try```、```catch``` 、```finally```,作爲例外處理的建構單元: 315 | 316 | ```clojure 317 | (defn divide-by [denom] 318 | (try 319 | (/ 1 denom) 320 | (catch ArithmeticException e 321 | (.printStackTrace e)) 322 | (finally 323 | (println "Divided by" denom)))) 324 | ``` 325 | 326 | 以上範例在 ```try``` 運算式內計算以 1 除以參數,如果發生 ```ArithmeticException``` 例外(除以 0),則會印出該例外的除錯堆疊資訊,```finally``` 則是不論 ```try``` 運算式是否產生例外,都會進入 ```finally``` 運算式內運行。 327 | 328 | Clojure 程式發生不可預期的錯誤時,可以使用 ```throw``` 來拋出例外: 329 | 330 | ```clojure 331 | (throw (IllegalStateException. "Illegal state exeption")) 332 | ;; => IllegalStateException Illegal state exeption 333 | ``` 334 | 335 | 值得注意的是,使用 ```throw``` 丟出的例外類型,必須是 ```java.lang.Throwable``` 或是它的子類別。 336 | 337 | ## 從 Java 呼叫 Clojure 338 | 339 | 大多數時間都是從 Clojure 端呼叫 Java,也會有要從 Java 端呼叫 Clojure 的時候。使用時首先引入 ```clojure.java.api.Clojure``` 類別,它提供了找尋 Var 物件、解析程式與存取命名空間的方法。 340 | 341 | 找到欲存取的 Var 物件後,以其實作的 IFn 介面進行調用,即可呼叫該 Var 物件代表的函式: 342 | 343 | ```java 344 | import clojure.java.api.Clojure; 345 | import clojure.lang.IFn; 346 | IFn plus = Clojure.var("clojure.core", "+"); 347 | plus.invoke(1, 2); 348 | ``` 349 | 350 | 或是引入 Clojure 的命名空間 351 | 352 | ```java 353 | IFn require = Clojure.var("clojure.core", "require"); 354 | require.invoke(Clojure.read("clojure.set")); 355 | ``` 356 | 357 | 或者是引入 Clojure 的高階函式來使用: 358 | 359 | ```java 360 | IFn map = Clojure.var("clojure.core", "map"); 361 | IFn inc = Clojure.var("clojure.core", "inc"); 362 | map.invoke(inc, Clojure.read("[1 2 3]")); 363 | ``` 364 | 365 | ## 回顧 366 | 367 | 從本篇文章中你了解到如何載入 Java 套件與類別,知道了讀取類別成員的方法,還有可以用來建立匿名與具名型別的巨集。還了解到如何將 Clojure 序列轉換成 Java 陣列的方式,以及例外處理的各個建構子。除此之外,還學會了如何由 Java 調用 Clojure 函式與資料的方式。 368 | 369 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 370 | -------------------------------------------------------------------------------- /08.md: -------------------------------------------------------------------------------- 1 | # 讀取器與詮釋資料 2 | 3 | > 你就要開始讀伊塔羅•卡爾維諾的新小說《如果在冬夜,一個旅人》。 4 | > 5 | > — 卡爾維諾《如果在冬夜,一個旅人》 6 | 7 | Clojure 程式開始於一串文字,經由讀取器 (Reader) 把程式轉化成資料結構;詮釋資料 (Metadata) 則是描述資料的資料,或者可以稱作元資料或中介資料。本篇文章將介紹讀取器以及詮釋資料的相關知識。 8 | 9 | ## 讀取器 (Reader) 10 | 11 | ### 形式 (Form) 12 | 13 | Clojure 程式的生命由一串文字開始,讀取器 (Reader) 將文字解析之後,產生出編譯器 (Compiler) 可以認識的資料結構。 14 | 15 | 讀取器嘗試將文字解析成形式 (Form) 或稱作運算式 (Expression),而形式 (Form) 是指任何可以順利被求值 (Evaluation) 的合法程式單元。 16 | 17 | 任何可以順利被求值的程式單元,包括下列幾種: 18 | 19 | #### 符號 (Symbol) 20 | 21 | 任何以非數字開頭的文字皆是符號,符號名稱可以有四則運算字符以及問號,它的型態爲 clojure.lang.Symbol。 22 | 23 | #### 常值 (Literal) 24 | 25 | 常值 (Literal) 指的是程式中代表固定值的連續字符。Clojure 中的常值共有以下幾種: 26 | 27 | - 字串 (String) 28 | 29 | 任何以雙引號 (") 包覆的字符會被看成字串,它的型態跟 Java 中的字串一樣,皆是 java.lang.String。 30 | 31 | - 數字 (Number) 32 | 33 | 以數字字符開頭的連續字符,共有整數 (Integer)、浮點數 (Float) 以及有理數 (Ratio)。型態分別爲 java.lang.Long、clojure.lang.BigInt、java.lang.Double 以及 clojure.lang.Ratio。 34 | 35 | - 字符 (Character) 36 | 37 | 字符以反斜線 (\\) 開頭,與實際的字符相對應。型態爲 java.lang.Character。 38 | 39 | - nil 40 | 41 | 代表虛無與不存在,與 Java 中的 null 相同意思。 42 | 43 | - 布林 (Boolean) 44 | 45 | 由 ```true``` 與 ```false``` 代表邏輯上的真與假。型態爲 java.lang.Boolean。 46 | 47 | - 關鍵字 (Keyword) 48 | 49 | 由冒號 (:) 開頭的連續字符被當作關鍵字,與符號類似,大半用作索引值。型態爲 clojure.lang.Keyword。 50 | 51 | #### 列表 (List) 52 | 53 | 以左右小括號 (```()```) 圍起,內部可以是任何形式 (Form)。型態爲 clojure.lang.PersistentList。 54 | 55 | #### 向量 (Vector) 56 | 57 | 以左右中括號 (```[]```) 圍起,內部可以是任何的形式 (Form)。型態爲 clojure.lang.PersistentVector。 58 | 59 | #### 映射 (Map) 60 | 61 | 以左右大括號 (```{}```) 圍起,內部是索引與值的對應關係,稱爲向量。索引與值可以是任何的形式 (Form)。型態爲 clojure.lang.PersistentHashMap、clojure.lang.PersistentArrayMap 或 clojure.lang.PersistentTreeMap。 62 | 63 | #### 集合 (Set) 64 | 65 | 以大括弧 (```{}```) 圍起任何形式,並在前面加上井號 (#) 被當作集合。型態爲 clojure.lang.PersistentHashSet 或 clojure.lang.PersistentTreeSet。 66 | 67 | ### 讀取巨集 (Reader Macro) 68 | 69 | 有一些字符經由讀取器解析時,會執行特殊的行爲,這些字符被稱爲讀取巨集 (Reader macro)。在 LISP 程式語言中,除了內建的讀取巨集之外,使用者還可以自定讀取巨集,用以改變讀取器的行爲。而在 Clojure 中,讀取巨集則無法讓使用者自行訂製。 70 | 71 | 以下列出 Clojure 中會被視爲讀取巨集的各種字符: 72 | 73 | #### 單引號 (') 74 | 75 | 如果單引號 (') 放置在任何符號前面,將會抑制 Clojure 對符號求值,將該符號原封不動返回,這種行爲稱爲引用 (Quote)。 與使用 ```quote``` 函式功能一樣。 76 | 77 | #### 反斜線 (\\) 78 | 79 | 而遇到反斜線 (\\) 時,讀取器則會將它其後字符返回,成爲字符常值 (Character literal)。 80 | 81 | #### 分號 (;) 82 | 83 | 解析到分號 (;) 則會將其後的字符忽略不做解析,是爲註解 (Comment)。 84 | 85 | #### 小老鼠符號 (@) 86 | 87 | 小老鼠符號則是會呼叫 ```deref``` 函式,取出其後的參數所引導的值,稱爲標的 (De-reference)。用在取出參考類型所儲存的值,或是等待由 ```promise``` 與 ```future``` 函式產生的延遲運算,計算完畢返回。 88 | 89 | #### 插入符號 (^) 90 | 91 | 插入符號 (^) 會伴隨着一個映射,其中是一些對於映射之後物件的描述資訊,這些資訊稱作詮釋資料 (Metadata)。與函式 ```with-meta``` 的功能一樣。 92 | 93 | 之後的小節將會有詮釋資料的詳細介紹。 94 | 95 | #### 井字符號 (#) 96 | 97 | 根據井字符號 (#) 之後的字符,讀取器會有不同的行爲,所以井字符號被稱作發派 (Dispatch) 巨集。以下是與井字符號搭配的各字符說明: 98 | 99 | - \#{} 100 | 101 | 集合。 102 | 103 | - \#"" 104 | 105 | 正則表達式。 106 | 107 | - \#' 108 | 109 | 傳回之後符號所代表的 Var 物件,與 ```var``` 函式相同。 110 | 111 | - \#() 112 | 113 | 匿名函式。 114 | 115 | - \#_ 116 | 117 | 之後的形式將會被讀取器忽略。 118 | 119 | #### 反引號 (`) 120 | 121 | 反引號 (`) (位置在鍵盤按鍵 1 左邊) 被稱爲語法引用 (Syntax quote),是 Clojure 巨集中使用的特殊符號之一,用來產生文字範本 (Template),範本中的形式將不會被求值。後續的章節將會有詳細的介紹。 122 | 123 | #### 波浪號 (~) 124 | 125 | 波浪號 (~) 被稱爲解引用 (Unquote),使用在反引號建立的文字範本內,讓波浪號後面跟隨的符號跳出範本而求值。 126 | 127 | 若波浪號 (~) 之後是小老鼠符號 (@),則被稱爲解引用拼接 (Unquote splice)。它的功用是將範本中的列表解消,替換成列表中的各個元素。 128 | 129 | ## 詮釋資料 (Metadata) 130 | 131 | 詮釋資料是添加在符號或群集中的映射,其中記載了該符號或群集的資訊。使用 ```with-meta``` 函式添加詮釋資料,它將返回添加了資料的物件;或用 ```meta``` 函式取得詮釋資料: 132 | 133 | ```clojure 134 | (with-meta [1 2 3] {:trivial true}) 135 | ;; => [1 2 3] 136 | (meta (with-meta [1 2 3] {:trivial true})) 137 | ;; => {:trivial true} 138 | ``` 139 | 140 | 或使用更簡便的方式,在映射前面加上插入符號 (^) 添加詮釋資料: 141 | 142 | ```clojure 143 | (def user ^{:birth "12-21"} {:name "Catherine"}) 144 | user 145 | ;; => {:name "Catherine"} 146 | (meta user) 147 | ;; => {:birth "12-21"} 148 | ``` 149 | 150 | 如果詮釋資料的映射中只有一個索引鍵與值的對應,而且值的內容爲真,則可以如以下的簡寫: 151 | 152 | ```clojure 153 | (def ^{:private true} x [1 2 3]) 154 | (def ^:private y [1 2 3]) 155 | ``` 156 | 157 | 以上的兩個符號都添加了私有的資訊,在其他的命名空間中無法取用。 158 | 159 | 若是用 ```def``` 或 ```defn``` 定義符號與 Var 物件時,在符號前面寫下詮釋資料,則詮釋資料將會被用在 Var 物件而不是符號,所以查看函式的詮釋資料必須查看儲存函式的 Var 物件,而不是符號: 160 | 161 | ```clojure 162 | (def ^{:doc "Nothing special"} x [1 2 3]) 163 | (meta x) 164 | ;; => nil 165 | (meta (var x)) 166 | ;; => {:doc "Nothing special", :line 1, :column 1, :file "/private/var/folders/5n/sm_s13cn3lb_p_4n2khqd0mr0000gn/T/form-init6817815229097680482.clj", :name x, :ns #namespace[user]} 167 | ``` 168 | 169 | 函式的說明文件也是利用詮釋資料的方式,添加到儲存函式的 Var 物件上。Var 物件的 ```:doc``` 索引鍵對應的值便是該物件的說明文件: 170 | 171 | ```clojure 172 | (defn doublex "Double the param" [x] (* x x)) 173 | (meta #'doublex) 174 | ;; => {:arglists ([x]), :doc "Double the param", :line 1, :column 1, :file "/private/var/folders/5n/sm_s13cn3lb_p_4n2khqd0mr0000gn/T/form-init6817815229097680482.clj", :name doublex, :ns #namespace[user]} 175 | ``` 176 | 177 | 從以上的範例可以看到,我們使用 ```meta``` 取得儲存函式 ```doublex``` 的 Var 物件的詮釋資料,其中的索引鍵 ```doc``` 便存放著定義函式時寫下的說明文件。 178 | 179 | Clojure 的核心函式也攜帶了豐富的詮釋資料,其中有該函式的命名空間、Var 物件的名稱、參數列表、說明文件、該函式何時加入 Clojure 等等的資訊。以下是 ```str``` 函式的詮釋資料: 180 | 181 | ```clojure 182 | (meta #'str) 183 | ;; => {:added "1.0", :ns #namespace[clojure.core], :name str, :file "clojure/core.clj", :static true, :column 1, :line 533, :tag java.lang.String, :arglists ([] [x] [x & ys]), :doc "With no args, returns the empty string. With one arg x, returns\n x.toString(). (str nil) returns the empty string. With more than\n one arg, returns the concatenation of the str values of the args."} 184 | ``` 185 | 186 | ## 特殊形式 (Special forms) 187 | 188 | 讀取器將一般文字轉換成一連串的形式之後,交給編譯器 (Compiler) 編譯成 Java 虛擬機位元碼,其中有一些形式的求值方法不同於一般的形式,稱爲特殊形式 (Special forms)。 189 | 190 | 舉例來說,前面章節提到過的 ```if``` 形式是一種特殊形式,它不像一般形式會在呼叫之前,先將各個參數求值,而是依據條件式的真與假,才決定對哪一個分支繼續求值。 191 | 192 | 特殊形式是 Clojure 程式語言的基石,所有的東西都是藉由特殊形式而打造出來。以下介紹各種特殊形式: 193 | 194 | ### def 195 | 196 | ```def``` 根據給予的符號名稱與資料,建立全域的 Var 物件。 197 | 198 | ```clojure 199 | (def a 10) 200 | ``` 201 | ### if 202 | 203 | 對 ```if``` 的第一個參數求值,若爲真則求值第二個運算式,否而且有第三個運算式則求值。 204 | 205 | ```clojure 206 | (if (= a 10) "true" "false") 207 | ;; => "true" 208 | ``` 209 | ### do 210 | 211 | 依序對 ```do``` 其後的各個運算式求值,並返回最後一個運算式求值的結果。 212 | 213 | ```clojure 214 | (do 215 | (println "Do") 216 | (str "Something:" 42) 217 | "else") 218 | ;; => Do 219 | ;; => "else" 220 | ``` 221 | 222 | ### let 223 | 224 | 以第一個參數向量中的符號與資料建立區域繫結,並對之後的運算式求值,區域繫結只在這些運算式有效。 225 | 226 | ```clojure 227 | (let [x 1 228 | y 2] 229 | y) 230 | ;; => 2 231 | ``` 232 | 233 | ### quote 234 | 235 | 不對其後的形式求值,原封不動地返回。 236 | 237 | ```clojure 238 | (quote (a 1 2)) 239 | ;; => (a 1 2) 240 | ``` 241 | 242 | Clojure 不會試圖去尋找以 ```a``` 爲名的函式並以參數呼叫,而是照實地返回。 243 | 244 | ### fn 245 | 246 | ```fn``` 建立函式,函式名稱是否提供都是可選的,之後是以類似 ```let``` 的向量參數繫結,參數之後則是函式的本體。 247 | 248 | Clojure 的函式實作了 Java 中的 ```Callable```、```Runnable``` 與 ```Comparator``` 三種介面。 249 | 250 | ```clojure 251 | (def triplex 252 | (fn this [x] 253 | (* x x x))) 254 | (triplex 3) 255 | ;; => 27 256 | ``` 257 | 258 | ### loop 259 | 260 | 與 ```let``` 一樣,差別在於建立了遞迴點 (Recursion point) 與 ```recur``` 搭配使用,用來反覆循環其中的運算式。 261 | 262 | ### recur 263 | 264 | 對跟隨在 ```recur``` 之後的各參數求值,以新值返回遞迴點重新執行。遞迴點可以藉由 ```loop``` 或 ```fn``` 建立。 265 | 266 | 你可以把 ```loop/recur``` 視爲顯式 (Explicit) 的尾遞迴 (Tail recursion)。 267 | 268 | ```clojure 269 | (def fib 270 | (fn [x] 271 | (loop [a 0 b 1 cnt x] 272 | (if (= cnt 0) 273 | a 274 | (recur (+' a b) a (dec cnt)))))) 275 | (fib 10) 276 | ;; => 55 277 | ``` 278 | 279 | ### throw 280 | 281 | 求值其後的運算式,並將得到的例外拋出。 282 | 283 | ```clojure 284 | (throw (Exception. "my exception message")) 285 | ;; => Exception my exception message 286 | ``` 287 | 288 | ### try 289 | 290 | ```try``` 有三個參數,第一個參數爲運算式本體,先對此運算式求值之後,若拋出例外且符合 ```catch``` 欲捕捉的例外,則執行 ```catch``` 的本體運算式。而不管是否有例外發生,```finally``` 的本體運算式都會被求值。 291 | 292 | ## 回顧 293 | 294 | 經由本篇文章,你了解了什麼是讀取器以及讀取巨集,還有讀取巨集中各個字符代表的特殊功能;還知道了詮釋資料的用途,比如添加或取得詮釋資料。更了解了奠定 Clojure 基礎的各種特殊形式。 295 | 296 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 297 | -------------------------------------------------------------------------------- /09.md: -------------------------------------------------------------------------------- 1 | # 並行與併發 2 | 3 | > 建構軟體設計有兩種方式: 4 | > 一種是簡單明顯地沒有缺陷,另一種則是複雜到沒有明顯的缺陷。 5 | > 6 | > — 東尼•霍爾 7 | 8 | 現代計算機系統走向多核,爲了運用多核心的能力,開始利用程式語言或作業系統提供的執行緒及處理序,將任務切割後同時處理。而不同任務間如果有共同的資源需要維護,增加了編寫程式人員的負擔。 9 | 10 | 一般的程式語言會使用鎖或是互斥器,避免不同程式同時存取相同資源,但是一旦使用不當便會發生死鎖或競爭條件等問題。而不同程式之間共享同一資源,更有可能造成兩邊資訊更新不一致。 11 | 12 | Clojure 中的不變性 (Immutable) 與持久存在 (Persistent),讓資料結構一旦建立就無法再更改並保持一定的效能,降低開發者面對錯綜複雜更新的風險。而設計巧妙的狀態管理,更減輕了開發者漸強的偏頭痛。 13 | 14 | 先來看看 Clojure 如何支援並行 (Parallelism)。 15 | 16 | ## 並行 17 | 18 | 並行 (Parallelism) 指的是同時有不同的程式分別去做各自任務,任務完成之後,將結果彙整起來。如果運行的環境具備多核心的能力,則任務便可以在不同的核心上執行,完成的時間將會減少。 19 | 20 | 既然 Clojure 建基在 Java 之上,自然可以使用 Java 中的執行緒類別,又由於 Clojure 中的函式實作了 Callable 與 Runnable 介面,使用起來自是容易許多: 21 | 22 | ```clojure 23 | (.start 24 | (Thread. 25 | (fn [] 26 | (Thread/sleep 3000) 27 | (println "Thread ends.")))) 28 | ;; => nil 29 | ;; 等待三秒 30 | ;; => Thread ends. 31 | ``` 32 | 33 | ### Future 34 | 35 | Clojure 提供了更簡便的方式讓你將函式置於執行緒中運行。使用 ```future``` 將欲完成的任務置於另一個執行緒中運行,呼叫 ```future``` 會返回 Future 物件並開始運行任務。 36 | 37 | 當任務完成後返回值則存放在 Future 物件之中,取得返回值則使用標的函式 ```deref``` 或小老鼠符號 ```@``` 於 Future 物件,即可取得任務執行後的結果。如任務尚未完成欲取得返回值,則會等待其計算完畢: 38 | 39 | ```clojure 40 | (def f (future (Thread/sleep 10000) (println "done") 100)) 41 | @f ;; 若在十秒內,此行將會停住等待計算完畢 42 | ;; => 100 43 | ``` 44 | 45 | 你可以使用 ```future-done?``` 檢查一個 ```Future``` 物件是否完成運行: 46 | 47 | ```clojure 48 | (def f (future (Thread/sleep 10000) (println "done") 100)) 49 | (future-done? f) ;; 十秒內運行 50 | ;; => false 51 | ;; 十秒後 52 | (future-done? f) 53 | ;; => true 54 | ``` 55 | 56 | 使用 ```future-cancel``` 則會將一個已經開始運行的 Future 物件終止,如果對一個已經被強制終止的 Future 物件取用標的 (deref),會拋出例外: 57 | 58 | ```clojure 59 | (def f (future (Thread/sleep 10000) (println "done") 100)) 60 | (future-cancel f) 61 | ;; => true 62 | @f 63 | ;; => CancellationException 64 | ``` 65 | 66 | ### Promise 67 | 68 | 利用 ```promise``` 建立的 Promise 物件則是與呼叫者建立約定,結果計算完畢之後會將它發送給持有 Promise 物件者。使用 ```promise``` 建立 Promise 物件、使用 ```deliver``` 傳送結果給 Promise 物件: 69 | 70 | ```clojure 71 | (def answer (promise)) 72 | (future (Thread/sleep 10000) (deliver answer 42)) 73 | ;; => #future[{:status :pending, :val nil} 0x2b9dc292] 74 | @answer ;; 十秒內運行的話,此行會停住 75 | ;; => 42 76 | ``` 77 | 78 | 與 Future 一樣,若 Promise 物件尚未接收到結果,取用標的 Promise 物件將會停住等待結果送到。你可以使用 ```realized?``` 於 Promise 物件上,來取得結果是否已送達: 79 | 80 | ```clojure 81 | (def p (promise)) 82 | (realized? p) 83 | ;; => false 84 | (deliver p :done) 85 | (realized? p) 86 | ;; => true 87 | ``` 88 | 89 | ### pmap 90 | 91 | 先前的章節已經看過的 ```map``` 函式,功能是將群集的各個元素套用到函式之中,產生新的群集。如果被套用的函式需要長時間的運算,等待所有元素都計算完畢就耗時過久。 92 | 93 | ```pmap``` 函式 (Parallel map) 提供升級的 ```map``` 功能,將每個元素的運算分給不同的執行緒,所有元素計算完畢再彙整起來,如果有夠多的計算核心,完成的時間越縮短。 94 | 95 | 以下的範例中有一個模擬運行耗時十秒的運算,分別套用到有十個元素的群集,使用 ```pmap``` 比起 ```map``` 效能提升顯著: 96 | 97 | ```clojure 98 | (def data [2 4 6 8 10 12 14 16 18 20]) 99 | (defn long-computaion [n] 100 | (Thread/sleep 10000) 101 | (* n 2)) 102 | 103 | (time (dorun (map long-computaion data))) 104 | ;; => "Elapsed time: 100031.777566 msecs" 105 | (time (dorun (pmap long-computaion data))) 106 | ;; => "Elapsed time: 10027.629015 msecs" 107 | ``` 108 | 109 | 可以看到以上範例中,原來的 ```map``` 版本以約略於 100 秒的時間完成,而進化的 ```pmap``` 版本由於受益於並行化,以近似 10 秒的時間完成。 110 | 111 | 範例中使用 ```dorun``` 強制對 ```map``` 返回的惰性序列求值,並以 ```time``` 函式計算運行花費的時間。 112 | 113 | 另外還有 ```pvalues``` 以及 ```pcalls``` 分別並行地對多個運算式求值,以及呼叫多個函式: 114 | 115 | ```clojure 116 | (pvalues (+ 3 2) (/ 2 3) (* 3 2) (- 32 23)) 117 | ;; => (5 2/3 6 9) 118 | (pcalls #(println "A long time ago in a galaxy far,") #(println "far away") #(println "....")) 119 | ;; => A long time ago in a galaxy far, 120 | ;; => far away … 121 | ;; => (nil nil nil). 122 | ``` 123 | 124 | ### Reducer 125 | 126 | 核心函式庫中的 ```map```、```filter```、```reduce``` (它還有另外一個名字:```fold```) 的作用是將一個群集轉換成另一個群集,雖然返回的是惰性序列仍然需要有創建的成本。 127 | 128 | 不同於核心函式庫的 ```reducer``` 函式庫,轉換的則不是資料結構,而是函式。不需要在一連串函式的轉換過程中創建暫時性的序列,而只是轉換運行的函式,此舉將會大大地增加效能。 129 | 130 | ```clojure.core.reducer``` 函式中的 ```map``` 與 ```filter``` 函式並不回傳惰性序列,而是傳回屆時可以做化約 (reducible) 的函式,稱爲 ```reducer```。 131 | 132 | 其中使用了 Java 7 中用以執行並行任務的框架:```Fork/Join```,將一連串的計算函式並行處理,減少處理時間。以下是典型的 ```map``` 與使用 ```reducer``` 後的各別效能評比: 133 | 134 | ```clojure 135 | (require '[clojure.core.reducers :as r]) 136 | 137 | (defn old-reduce [nums] 138 | (reduce + (filter even? (map inc nums)))) 139 | 140 | (defn new-fold [nums] 141 | (r/fold + (r/filter even? (r/map inc nums)))) 142 | 143 | (time (old-reduce (vec (range 1000000)))) 144 | ;; => "Elapsed time: 136.409418 msecs" 145 | ;; => 250000500000 146 | (time (new-fold (vec (range 1000000)))) 147 | ;; => "Elapsed time: 96.708929 msecs" 148 | ;; => 250000500000 149 | ``` 150 | 151 | ## 狀態管理與併發 152 | 153 | 併發 (Concurrency) 是指同時有數個執行單元會交互執行,通常會牽涉一些共享的資源以及互相協作。 154 | 155 | 其實 Clojure 並沒有提供併發相關的函式或巨集,它提供了經過妥善設計的狀態管理方法,讓不同執行單元之間共享資源更容易管理且不易出錯。 156 | 157 | 在介紹狀態管理方法之前,先來了解 Clojure 對於事物的世界觀。 158 | 159 | ### 身份與狀態 160 | 161 | 在 Clojure 世界中,一件事物分成身份 (Identity) 與狀態 (State),在時間的長河裡,每件事物在不同的時間有不同的狀態,狀態以值表示,由於值在 Clojure 世界中具有不變性,因此無法對值進行改變。如果要取得事物的狀態,則必須透過身份來取得,但是取得的狀態只是某個時間中狀態的快照 (Snapshot)。 162 | 163 | 例如一位名爲 Catherine 的使用者,20 歲時剛畢業開始工作,30 歲時結婚,不同的時間有不同的狀態,但是都是同一個身份。如果在傳統的程式語言,身份與狀態是含混不清的: 164 | 165 | ```java 166 | catherine.age = 20; 167 | catherine.graduated = true; 168 | ;; 時間經過十年... 169 | catherine.age += 10; 170 | catherine.married = true; 171 | ``` 172 | 173 | 上面的範例中,一個使用者類型既是代表某一種身份,更混雜了狀態。在 Clojure 中,狀態儲存在四種參考類型中,透過函式取得其中的狀態,新的狀態也是經由函式加上舊的狀態產生而成。而新的狀態在新的時間中,並不會影響其它時間的狀態。 174 | 175 | 若是有人在 20 秒前取得某個身份的狀態,10 秒後這個身份改變了狀態,之前取得的狀態並不會改變,保證了時間軸上狀態的一致。 176 | 177 | ### 參考類型 178 | 179 | Clojure 使用四種類型管理狀態,這些類型稱爲參考類型 (Reference type)。四種參考類型分別又隸屬於兩種分類:協作式 (Coordinated) 與同步式 (Synchronous),協作式指的是不同狀態更新時需要協調合作,同步式則是指在更新狀態前有可能會被阻攔或停滯,因爲其他部分正在更新,所以必須等待。 180 | 181 | 以下以圖形表示參考類型所屬的分類: 182 | 183 | ```clojure 184 | ;; | Coordinated | Uncoordinated 185 | ;; ------|-------------|-------------- 186 | ;; Sync | Ref | Atom 187 | ;; Async | N/A | Agent 188 | ``` 189 | 190 | #### Ref 191 | 192 | Clojure 使用了軟體事務存儲 (Software Transactional Memory,之後簡稱 STM) 模型,來處理併發與狀態管理。STM 類似於資料庫,只是它存在於記憶體之中,僅能保證 ACID 中的三種:不可分割性 (Atomicity)、一致性 (Consistency) 與隔離性 (isolation),並不保證持久性 (Durability)。 193 | 194 | 在 Clojure 中,一旦進入改變狀態的事務交易 (Transaction) 環境裡,如果其中的變化有一個不成功,則會退出視爲失敗。而其它存取狀態的執行單元並不知道交易的狀態,只會看到交易前的情形。 195 | 196 | 而 Ref 參考類型就是 Clojure 根據 STM 的實作,透過 ```ref``` 函式創建內含有狀態的 Ref 類型: 197 | 198 | ```clojure 199 | (def account (ref 0)) 200 | ;; => #'user/account 201 | ``` 202 | 203 | 要取得參考類型的值,都是使用 ```deref``` 函式或小老鼠符號 ```@```: 204 | 205 | ```clojure 206 | (deref account) 207 | ;; => 0 208 | (+ 5 @account) 209 | ;; => 5 210 | ``` 211 | 212 | 你可以使用 ```ref-set``` 更新 Ref 狀態: 213 | 214 | ```clojure 215 | (ref-set account 500) 216 | ;; => IllegalStateException No transaction running 217 | ``` 218 | 219 | 以上範例出現的例外告訴我們,更新 Ref 狀態必須在交易 (Transaction) 中進行。建立可以安心運作的交易環境使用 ```dosync```: 220 | 221 | ```clojure 222 | (dosync 223 | (ref-set account 500)) 224 | ;; => 500 225 | @account 226 | ;; => 500 227 | ``` 228 | 229 | 現在 Ref 已更新狀態。除了 ```ref-set``` 之外,還提供以函式方式更新狀態的方法: 230 | 231 | ```clojure 232 | (dosync 233 | (alter account + 500)) 234 | ;; => 1000 235 | @account 236 | ;; => 1000 237 | ``` 238 | 239 | ```alter``` 的第一個參數是 Ref 類型,之後則是函式與準備帶給函式的參數,函式將會以下列的方式呼叫: 240 | 241 | ```clojure 242 | (apply fun value-in-ref args) 243 | ``` 244 | 245 | ```dosync``` 創造的交易環境確保在其中進行更新的多個 Ref,在交易完成之後將會同步更新。如果在交易之中,其中一個 Ref 的狀態已經被外部改變,整個交易便會重啓,利用新的狀態再進行改變。 246 | 247 | ```clojure 248 | (def debit (ref 100000)) 249 | (def account (ref 1000)) 250 | (dosync 251 | (alter debit - 1500) 252 | (alter account + 1500)) 253 | ;; => 2500 254 | @debit 255 | ;; => 98500 256 | @account 257 | ;; => 2500 258 | ``` 259 | 260 | 如果不想因爲交易內容改變而重啓,而且交易中的執行內容不會因爲順序改變而不同,則可以考慮 ```commute``` 函式: 261 | 262 | ```clojure 263 | (dosync 264 | (commute debit + 500) 265 | (commute account + 500)) 266 | ;; => 3000 267 | @debit 268 | ;; => 99000 269 | @account 270 | ;; => 3000 271 | ``` 272 | 273 | 將 Ref 添加驗證器 (Validator) 函式會確保在更新狀態的時候,符合驗證器函式的內容,若不符合則會返回之前的狀態: 274 | 275 | ```clojure 276 | (defn validate-account 277 | [state] 278 | (not (neg? state))) 279 | 280 | (def bank-account (ref 1000 :validator validate-account)) 281 | (dosync 282 | (alter bank-account - 1500)) 283 | ;; => IllegalStateException Invalid reference state 284 | @bank-account 285 | ;; => 1000 286 | ``` 287 | 288 | #### Atom 289 | 290 | 原子類型 (Atom) 與 Ref 類型一樣都是屬於同步式:更新原子類型時必須等候先前的狀態更新完成,但是原子類型則不是協作式的,亦即無法同時更新兩個以上的原子類型,每個原子類型的更新都是與其他原子隔離的。 291 | 292 | 使用 ```atom``` 函式創造內含有狀態的原子類型,取得原子類型的值,也是使用 ```deref``` 函式或小老鼠符號 ```@```: 293 | 294 | ```clojure 295 | (def x (atom 10)) 296 | ;; => #'user/x 297 | @x 298 | ;; => 10 299 | ``` 300 | 301 | 你可以使用 ```reset!``` 更新原子中的狀態: 302 | 303 | ```clojure 304 | (reset! x 20) 305 | ;; => 20 306 | @x 307 | ;; => 20 308 | ``` 309 | 310 | 也可以透過類似 ```alter``` 函式的 ```swap!```,以更新函式來更新原子類型中的狀態: 311 | 312 | ```clojure 313 | (def catherine (atom {:name "Catherine" :age 18 :graduated? false})) 314 | ;; => #'user/catherine 315 | (swap! catherine update-in [:age] + 2) 316 | ;; => {:name "Catherine", :age 20, :graduated? false} 317 | (swap! catherine update-in [:graduate?] not) 318 | ;; => {:name "Catherine", :age 20, :graduated? true} 319 | @catherine 320 | ;; => {:name "Catherine", :age 20, :graduated? true} 321 | ``` 322 | 323 | 以上範例 ```swap!``` 以 ```update-in``` 函式更新原子類型中的映射,它會以下面的呼叫方式更新映射: 324 | 325 | ```clojure 326 | (update-in @catherine [:age] + 2) 327 | ``` 328 | 329 | Clojure 提供了一個更新原子類型的函式,可以在更新之前先檢查狀態是否和預期的相等,若相等則更新至新值,否則不做改變: 330 | 331 | ```clojure 332 | (def x (atom 10)) 333 | (compare-and-set! x 20 30) 334 | ;; => false 335 | @x 336 | ;; => 10 337 | (compare-and-set! x 10 20) 338 | ;; => true 339 | @x 340 | ;; => 20 341 | ``` 342 | 343 | 原子類型也可以像 Ref 類型一樣添加驗證器,驗證欲改變的狀態是否符合驗證規則: 344 | 345 | ```clojure 346 | (def x (atom 100 :validator pos?)) 347 | (swap! x + 500) 348 | ;; => 600 349 | (swap! x - 700) 350 | ;; => IllegalStateException Invalid reference state 351 | ``` 352 | 353 | 除了驗證器之外,還可以對四種參考類型添加觀察者函式 (Watch function),一旦更新了參考類型的狀態,觀察者函式便會被呼叫。使用 ```add-watch``` 函式添加觀察者: 354 | 355 | ```clojure 356 | (add-watch x :echo 357 | (fn [key ref old new] 358 | (println "Key:" key) 359 | (println "Reference:" ref) 360 | (println "Old:" old) 361 | (println "New:" new))) 362 | ;; => #atom[600 0x1bce3aea] 363 | ``` 364 | 365 | ```add-watch``` 第一個參數是準備觀察的參考類型,第二個參數則是代表新的觀察者的關鍵字標識,你可以使用這個關鍵字來參照到新的觀察者函式。 366 | 367 | 最後一個參數是欲添加的觀察者函式,觀察者函式必須接受四個參數:觀察者的關鍵字標識、觀察的參考類型、舊狀態、新狀態。 368 | 369 | 觀察者函式加上去之後,狀態一旦改變,觀察者函式便會被呼叫: 370 | 371 | ```clojure 372 | (reset! x 300) 373 | ;; => Key: :echo 374 | ;; => Reference: #atom[300 0x1bce3aea] 375 | ;; => Old: 600 376 | ;; => New: 300 377 | ;; => 300 378 | ``` 379 | 380 | 觀察完畢後,透過觀察者的關鍵字標識與函式 ```remove-watch```,可以把觀察者函式從參考類型中移除: 381 | 382 | ```clojure 383 | (remove-watch x :echo) 384 | ;; => #atom[300 0x1bce3aea] 385 | (reset! x 500) 386 | ;; => 500 387 | ``` 388 | 389 | 從以上範例可以看到,觀察者函式移除之後,更新狀態就不會出現觀察者函式的訊息了。 390 | 391 | #### Agent 392 | 393 | 有別於 Ref 與原子類型的協調式與同步式,Agent 類型狀態的更新不需與其他狀態更新協同合作,也不需等候其他更新完成。通常用在需要更新狀態,卻不需要關心更新後的結果。 394 | 395 | 使用 ```atom``` 函式創造內含有狀態的 Agent 類型,取得 Agent 類型的值,也是使用 ```deref``` 函式或小老鼠符號 ```@```: 396 | 397 | ```clojure 398 | (def a (agent 5)) 399 | ;; =>#'user/a 400 | @a 401 | ;; => 5 402 | ``` 403 | 404 | 可以使用 ```send``` 函式更新 Agent 類型中的狀態,使用方法與 ```alter``` 以及 ```swap!``` 類似: 405 | 406 | ```clojure 407 | (send a + 100) 408 | ;; => #agent[{:status :ready, :val 105} 0x11fdbaa4] 409 | @a 410 | ;; => 105 411 | ``` 412 | 413 | 不同於 Ref 與原子類型更新函式的返回值,```send``` 返回的是狀態更新後的 Agent 類型。 414 | 415 | 另外還有 ```send-off``` 提供與 ```send``` 函式同樣的使用方法: 416 | 417 | ```clojure 418 | (send-off a + 150) 419 | ;; => #agent[{:status :ready, :val 255} 0x11fdbaa4] 420 | @a 421 | 255 422 | ``` 423 | 424 | Agent 類型會將更新狀態的動作放入佇列中,啓動執行緒逐個處理。```send-off``` 與 ```send``` 提出的更新動作分別放入不同的佇列,分別由不同的執行緒集區 (Thread pool) 中的執行緒處理佇列,處理 ```send-off``` 動作佇列的執行緒集區會依據需要擴增,而處理 ```send``` 動作佇列的執行緒集區則是固定大小。 425 | 426 | 因此建議使用 ```send-off``` 處理會阻塞的操作,例如 IO。以免被過多的操作卡住無法從執行緒集區中分配新的執行緒來處理。 427 | 428 | #### Var 429 | 430 | Var 物件已經是我們非常熟悉的參考類型。使用 ```def``` 創建內含資料的 Var 物件: 431 | 432 | ```clojure 433 | (def xx 1) 434 | ``` 435 | 436 | 建立了 Var 物件之後,Var 物件被賦予的值被稱爲根繫結 (root binding),它是全域可見的,所有的執行緒都可以取得: 437 | 438 | ```clojure 439 | (.start 440 | (Thread. 441 | (fn [] (println "xx:" xx)))) 442 | ;; => xx: 1 443 | ``` 444 | 445 | 若在創建 Var 物件時加上特殊的詮釋資料 (Metadata),便可以動態改變 Var 物件的內容: 446 | 447 | ```clojure 448 | (def ^:dynamic *xx* 111) 449 | (binding [*xx* 222] 450 | *xx*) 451 | ;; => 222 452 | *xx* 453 | ;; => 111 454 | ``` 455 | 456 | 以上範例使用 ```binding``` 在其本體運算式內重新繫結了 Var 物件,因此在 ```binding``` 作用區之內,Var 物件的內容值改變了,一旦離開作用區域,Var 物件又回到根繫結的值。 457 | 458 | 使用 ```alter-var-root``` 可以將 Var 物件的根繫結換成新的值: 459 | 460 | ```clojure 461 | (.start 462 | (Thread. 463 | (fn [] 464 | (alter-var-root (var *xx*) (fn [_] 333)) 465 | (println "*xx* is now" *xx*)))) 466 | ;; => *xx* is now 333 467 | *xx* 468 | ;; => 333 469 | ``` 470 | 471 | ```alter-var-root``` 接受一個將被改變根繫結的 Var 物件,以及一個函式,該函式的返回值便是此 Var 物件根繫結的新值。 472 | 473 | Clojure 不建議在並行或併發的環境下使用 Var 物件做資源的共享,因爲各執行緒之間無法做好協調。執行緒 A 中取得的值,可能在執行緒 B 中已經做了修改。 474 | 475 | ## 回顧 476 | 477 | 經由本篇文章,你學會了 Clojure 支援並行的方式,以及與併發之間的差別。還知道了如何以並行方式更快完成工作,也學會了如何使用四種參考型別來管理狀態,以及他們之間的特色與差別。 478 | 479 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 480 | 481 | -------------------------------------------------------------------------------- /11.md: -------------------------------------------------------------------------------- 1 | # 巨集 2 | 3 | > 授人以 Fortran 得 Fortran, 4 | > 授人以 Lisp 得所喜之語言。 5 | > 6 | > — 蓋伊·史提爾二世《The Seasoned Schemer》 7 | 8 | LISP 程式語言家族的編寫方式與編譯器內部使用的語法樹相似,這種特色被稱爲同像性 (Homoiconicity)。依據這項特色,產生了有別於其他語言的魔法,可以編寫程式來改變程式,不僅可以創造自己的語言,還可以擴充程式語言,彌補程式語言的不足。 9 | 10 | 這個神奇的魔法就是巨集 (Macro),透過這篇文章你將學會如何撰寫巨集。 11 | 12 | ## 什麼是巨集 13 | 14 | ### 基礎認識 15 | 16 | Clojure 的資料結構之一:列表,與 Clojure 程式非常像。我們可以透過 ```list``` 創建列表 17 | 18 | ```clojure 19 | (list '+ 1 2) 20 | ;; => (+ 1 2) 21 | (class (list '+ 1 2)) 22 | ;; => clojure.lang.PersistentList 23 | ``` 24 | 25 | 範例中的運算式產生了一個列表,內容爲符號 ```+``` 以及 1 與 2。與運算式 ```(+ 1 2)``` 看起來一模一樣,差別是使用 ```list``` 函式產生的資料。 26 | 27 | 前面章節介紹過的讀取器 (Reader) 就是將文字轉換成列表後,再做進一步的處理。我們可以使用 ```read-string``` 模擬讀取器,將文字轉換成列表: 28 | 29 | ```clojure 30 | (read-string "(+ 1 2)") 31 | ;; => (+ 1 2) 32 | (class (read-string "(+ 1 2)")) 33 | ;; => clojure.lang.PersistentList 34 | ``` 35 | 36 | ```read-string``` 函式讀取文字,轉換成列表。透過 ```eval``` 函式,可以把列表當作程式執行,求得執行後的結果: 37 | 38 | ```clojure 39 | (eval (list + 1 2)) 40 | ;; => 3 41 | ``` 42 | 43 | 也可以使用單引號 (') 於列表之前,Clojure 會將列表原封不動返回: 44 | 45 | ```clojure 46 | '(+ 1 2) 47 | ;; => (+ 1 2) 48 | (eval '(+ 1 2)) 49 | ;; => 3 50 | ``` 51 | 52 | ### 引用 (Quote) 53 | 54 | 單引號 (') 被稱爲引用 (Quote),它之後的列表會被 Clojure 忽略而照實返回,如果將它交給 ```eval``` 函式則會被求值。列表若是沒有在前面加上單引號,Clojure 便會將它視爲函式呼叫,呼叫第一個符號代表的函式,如果找不到函式便會報錯: 55 | 56 | ```clojure 57 | (a 1 2) 58 | ;; => CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context 59 | '(a 1 2) 60 | ;; => (a 1 2) 61 | (eval '(a 1 2)) 62 | ;; => CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context 63 | ``` 64 | 65 | 使用引用 (Quote) 讓 Clojure 不對列表求值,將程式變成資料,再丟給 ```eval``` 函式把資料當作可執行的函式,執行之後取得返回值。 66 | 67 | 於是藉由列表與引用,可以產生程式的範本,其中包含了之後將被執行的程式碼,這些範本只是一般的資料,可以任意地拼接或修改成任意列表,最終再丟給 ```eval``` 或編譯器編譯後執行: 68 | 69 | ```clojure 70 | (cons '+ '(1 2)) 71 | ;; => (+ 1 2) 72 | (eval (cons '+ '(1 2 3))) 73 | ;; => 6 74 | ``` 75 | 76 | 現在你應該更了解在 Clojure 中,程式可以是資料,資料也可以是程式的意思了。前面範例使用 ```eval``` 則讓我們了解 Clojure 如何將文字變成列表,再將列表變成程式執行的概念模型。 77 | 78 | ### 產生程式範本 79 | 80 | 現在可以依據需要,產生列表給 ```eval``` 執行後取得結果,現在讓我們把產生列表的功能寫成函式,以便重複使用。 81 | 82 | ```clojure 83 | (defn report [form] 84 | (eval (cons 'println (list "report form:" form)))) 85 | 86 | (report (= 2 (+ 2 3))) 87 | ;; => report form: false 88 | ``` 89 | 90 | 原本我們預期的結果應該是:```report form: (= 2 (+ 2 3))```,結果卻不如預期,這是爲什麼呢? 91 | 92 | 原因在於 Clojure 中 (大部分程式語言也是一樣),在呼叫函式之前,程式語言會先把交給函式的參數計算求值完畢,函式再根據參數計算。 93 | 94 | 因此在我們的範例中 ```(= 2 (+ 2 3))``` 便會先求值 (結果爲 false),結果便與我們想要的不一樣。因此如果要達到如此的效果,就要使用巨集。 95 | 96 | 另外,```eval``` 函式雖然可以幫忙我們將列表轉成程式後求值,但是它無法處理當呼叫 ```eval``` 函式時的詞法語境 (Lexical scope),亦即當前可以使用的環境 (繫結或符號等等)。因此使用範圍非常侷限。 97 | 98 | 從以上範例我們使用 ```eval``` 模擬了編譯器將程式求值的行爲,我們也使用了 ```read-string``` 模擬了將文字轉換成列表的行爲,也了解到修改列表就可以改變程式的行爲。 99 | 100 | 而巨集便是可以修改程式、改變行爲的工具。 101 | 102 | ## 構建巨集 103 | 104 | ### defmacro 105 | 106 | 建立函式使用 ```defn``` 或是 ```fn```,而建立巨集則使用 ```defmacro```。```defmacro``` 的參數不會預先求值,會返回列表給呼叫巨集者,編譯器會轉成實際程式執行之: 107 | 108 | ```clojure 109 | (defmacro infix-add [form] 110 | (list (second form) (first form) (last form))) 111 | 112 | (infix-add (2 + 3)) 113 | ;; => 5 114 | ``` 115 | 116 | 以上範例利用巨集來讀取以中置表示法寫成的形式,並求值之。我們可以使用 ```macroexpand``` 觀察巨集如何展開: 117 | 118 | ```clojure 119 | (macroexpand '(infix-add (2 + 3))) 120 | ;; => (+ 2 3) 121 | ``` 122 | 123 | ### 語法引用 124 | 125 | 反引號 (`) 被稱爲語法引用 (Syntax quote),功能與單引號 (') 類似,差別在於反引號之後的符號會被改成加上命名空間後的全名 (Qualified name),避免衝突。 126 | 127 | ```clojure 128 | (def foo "foo") 129 | ;; => #'user/foo 130 | 'foo 131 | ;; => foo 132 | `foo 133 | ;; => user/foo 134 | ``` 135 | 136 | 以上範例分別使用引用以及語法引用,引用只返回符號,語法引用則會將符號加上命名空間。建議編寫巨集時使用語法引用,避免衝突問題,而且語法引用也可以與之後介紹的解引用搭配使用。 137 | 138 | ### 解引用 139 | 140 | 另一個語法引用 (~) 與引用 (') 的差別,在於語法引用中可以使用波浪號 (~),波浪號稱爲解引用 (Unquote)。將波浪號加在語法引用中的其中一個列表或符號之前,將會使這個符號或列表跳出引用環境,讓編譯器對符號或列表求值,再將它放回引用環境中,產出新的列表。 141 | 142 | 以下範例示範加上解引用之前與之後的差別: 143 | 144 | ```clojure 145 | `(+ 1 (* 2 3)) 146 | ;; => (clojure.core/+ 1 (clojure.core/* 2 3)) 147 | `(+ 1 ~(* 2 3)) 148 | ;; => (clojure.core/+ 1 6) 149 | ``` 150 | 151 | 第一個範例中只在列表前加上語法引用,於是列表中的符號加上命名空間後,便返回了。而第二個範例中將解引用加在 ```(* 2 3)``` 之前,此運算式就會先被求值,再放回語法引用建立的列表之中。 152 | 153 | 現在我們把之前提過的 ```report``` 範例用巨集與語法引用改寫: 154 | 155 | ```clojure 156 | (defmacro report [form] 157 | `(println "report form:" '~form)) 158 | (report (= 2 (+ 2 3))) 159 | ;; => report form: (= 2 (+ 2 3)) 160 | ``` 161 | 162 | 看起來一切正常,但是出現了先前沒有出現的符號,究竟是什麼意思呢?先讓我們利用 REPL 看看它到底做了什麼事: 163 | 164 | ```clojure 165 | (def a 4) 166 | `(1 2 3 '~a) 167 | ;; => (1 2 3 (quote 4)) 168 | ``` 169 | 170 | 它先對 a 求值,結果爲 4,再把它套用到 ```quote``` 之中。因此在我們的 ```report``` 範例中,```'~``` 符號會對 ```form``` 求值,form 的值爲 ```(= 2 (+ 2 3))```,再使用 ```quote``` 抑制函式呼叫後放回語法引用產生的範本中。 171 | 172 | 再擴充 ```report``` 巨集,讓它除了可以顯示原始的形式之外,還可以對這個形式求值。因此會再次使用到解引用,以求得形式的值: 173 | 174 | ```clojure 175 | (defmacro report [form] 176 | `(println "report form:" '~form ", result:" ~form)) 177 | 178 | (report (= 3 (+ 2 1))) 179 | ;; => report form: (= 3 (+ 2 1)) , result: true 180 | ``` 181 | 182 | ### 解引用拼接 183 | 184 | 除了解引用之外,還有一種特殊的解引用稱爲解引用拼接 (Unquote-splicing),使用 ```~@``` 符號來達到此功能。 185 | 186 | 解引用拼接 (Unquote-splicing) 會將之後的列表解開,再放入其他的列表之中: 187 | 188 | ```clojure 189 | (def a '(5 6 7 8)) 190 | `(1 2 3 4 ~a 9 10) 191 | ;; => (1 2 3 4 (5 6 7 8) 9 10) 192 | `(1 2 3 4 ~@a 9 10) 193 | ;; => (1 2 3 4 5 6 7 8 9 10) 194 | ``` 195 | 196 | 第一個範例中使用解引用,結果是列表中又有列表,而第二個範例用了解引用拼接之後,解消了原先的列表,將列表中的每個元素放入新的列表之中。 197 | 198 | 解引用拼接通常用在巨集接受不定個數的運算式,以下範例示範其使用方法: 199 | 200 | ```clojure 201 | (defmacro foo [& body] 202 | `(do-something ~@body)) 203 | (macroexpand-1 '(foo (do (println "Hello foo") 42))) 204 | ;; => (user/do-something (do (println "Hello foo") 42)) 205 | ``` 206 | 207 | 由於巨集中已經使用了列表,所以需要先將參數套用解引用拼接,再放入列表中。範例中使用的 ```macroexpand-1``` 功能與先前提及的 ```macroexpand``` 一樣,都是具有展開巨集的功能,而 ```macroexpand``` 展開的巨集之中若還有使用到其他巨集,則會繼續展開。 208 | 209 | ### gensym 210 | 211 | 由於巨集具有範本化程式的功能,在巨集之中若有資料繫結,一不注意就會發生錯誤: 212 | 213 | ```clojure 214 | (defmacro bad-macro [& body] 215 | `(let [x :value] 216 | ~@body)) 217 | (bad-macro (println "bad macro")) 218 | ;; => CompilerException java.lang.RuntimeException: Can't let qualified name: user/x 219 | (macroexpand-1 '(bad-macro (println "bad macro"))) 220 | ;; => (clojure.core/let [user/x :value] (println "bad macro")) 221 | ``` 222 | 223 | 以上巨集範例中使用到資料繫結,被編譯器發現錯誤,因爲沒有使用暫時的名字,而使用全名。若是使用該巨集的環境中已經有相同名稱的資料繫結,就有發生錯誤的可能。 224 | 225 | Clojure 提供了 ```gensym``` 函式產生編造過的符號名稱,避免與其他符號名稱衝突的可能,以下是使用 ```gensym``` 修改過的版本: 226 | 227 | ```clojure 228 | (defmacro good-macro [& body] 229 | (let [x (gensym)] 230 | `(let [~x :value] 231 | ~@body))) 232 | 233 | (good-macro (println "good macro")) 234 | ;; => good macro 235 | (macroexpand-1 '(good-macro (println "good macro"))) 236 | ;; => (clojure.core/let [G__10556 :value] (println "good macro")) 237 | ``` 238 | 239 | 你也可以在語法引用之中,於符號之後加上井字號,功能與使用 ```gensym``` 相同: 240 | 241 | ```clojure 242 | (defmacro hygienic-macro [& body] 243 | `(let [x# :value] 244 | ~@body)) 245 | 246 | (hygienic-macro "hygienic macro") 247 | ;; => "hygienic macro" 248 | (macroexpand-1 '(hygienic-macro "hygienic macro")) 249 | ;; => (clojure.core/let [x__10558__auto__ :value] "hygienic macro") 250 | ``` 251 | 252 | ## 巨集可以做什麼 253 | 254 | 現在你手上已經有了建構巨集的工具,然而究竟巨集可以做到哪些事,以下提出幾項範例,希望給讀者一些靈感。 255 | ### 定義控制流程 256 | 257 | Ruby 或 Perl 程式語言提供了與 ```if``` 相反的控制流程:```unless```,只有 ```unless``` 中的判斷式爲否,才會執行第一個分支,否則不執行: 258 | 259 | ```clojure 260 | (unless (= 1 2) "Math rules !") 261 | ;; => "Math rules !" 262 | ``` 263 | 264 | ```unless``` 可以利用 ```if``` 達成相同的行爲: 265 | 266 | ```clojure 267 | (if (not conditional) then) 268 | ``` 269 | 270 | 因此 ```unless``` 巨集如下: 271 | 272 | ```clojure 273 | (defmacro unless [conditional & body] 274 | `(if (not ~conditional) 275 | (do ~@body))) 276 | ``` 277 | 278 | 以上巨集使用語法引用建立範本,使用解引用對條件式求值再放回列表,再使用解引用拼接處理巨集剩餘的參數。 279 | 280 | ### 資源管理 281 | 282 | 你是否曾經在程式中開啓檔案,卻在結束時忘記將檔案關閉,導致記憶體資源浪費?建立一個在運算式最後自動關閉資源的巨集,是非常方便的: 283 | 284 | ```clojure 285 | (with-open [r (clojure.java.io/input-stream "tmpfile.txt")] 286 | (println "Do things with opened resource")) 287 | ``` 288 | 289 | 範例如下: 290 | 291 | ```clojure 292 | (defmacro with-my-open [bindings & body] 293 | `(let ~(subvec bindings 0 2) 294 | (try 295 | ~@body 296 | (finally 297 | (. ~(bindings 0) close))))) 298 | ``` 299 | 300 | 以上範例爲簡易版,Clojure 內建有 ```with-open``` 巨集,考慮更周全,請使用內建版本。 301 | 302 | ## 給讀者的忠告 303 | 304 | 由於巨集可以修改語法、改變列表結構,所以巨集的能力只受限於使用者的想像力。但是越是強大的工具,越要謹慎使用。以下有幾點建議: 305 | 306 | 1. 可以用函式不要用巨集 307 | 308 | 巨集除錯困難,可以用函式就不需要巨集,除非需要使用到延遲求值功能或建立自己的語法。 309 | 310 | 2. 使用 ```macroexpand``` 或 ```macroexpand-1``` 以及 ```clojure.walk/macroexpand-all``` 除錯 311 | 312 | 一旦巨集不如預期運作,使用 ```macroexpand``` 相關函式將巨集展開,觀察展開過的列表究竟何處發生問題。 313 | 314 | 3. 參考別人的巨集 315 | 316 | 要寫好程式除了了解語言的特性與語法之外,研讀別人的程式碼也是進步的方式之一。Clojure 內建許多巨集,可以在 REPL 中使用 ```source``` 函式列出巨集的原始碼,學習其中的思考方式。 317 | 318 | ## 回顧 319 | 320 | 透過本篇文章,你知道了 Clojure 中的運算式都是由列表構成,還知道了在編譯之前修改列表,就可以改變編譯的結果。了解到藉由定義巨集可以達成自定語法,也了解了建構巨集的相關工具,還知道了巨集可以如何運用,最後你知道了撰寫巨集應該注意的地方。 321 | 322 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 323 | -------------------------------------------------------------------------------- /12.md: -------------------------------------------------------------------------------- 1 | # 測試 2 | 3 | 你是否有過這樣的經驗:本來只是修改了 A 部分的程式,結果改完之後 B 部分的程式竟然不正常;或者是以前早就改好的問題,在這次改版之後又出現了呢?當有這些情形出現,你需要的是透過測試來提早檢查可能出錯的地方,並找出會導致出錯的條件。 4 | 5 | 除了人工測試之外,最應該加入的就是有一組編寫好的測試程式。有了這些測試程式,你可以在正式上線或提交程式碼之前,利用這些測試程式檢查新修改的程式是否符合規定,也可以保證過往的問題不再出現。 6 | 7 | 本文就跟大家介紹在 Clojure 專案中撰寫測試的方法。使用的工具是內建的 `clojure.test`。 8 | 9 | ## 事前準備 10 | 11 | 使用 `clojure.test` 之前,要記得在測試程式中將 `clojure.test` 的命名空間引入,如下所示: 12 | 13 | ```clojure 14 | (ns clj-test.core-test 15 | (:require [clojure.test :refer :all])) 16 | ``` 17 | 18 | 或在 REPL 中載入 `clojure.test` 命名空間: 19 | 20 | ```clojure 21 | (require '[clojure.test :refer :all]) 22 | ``` 23 | 24 | 如果使用 leiningen 建立專案,專案中已經有一個 test 目錄,其中有自動產生的空白測試程式碼範本,把測試程式寫在那裏就對了。 25 | 26 | ## 斷言 27 | 28 | ### is 29 | 30 | 像其他的測試框架一樣,`clojure.test` 提供了斷言 (Assertion) 用來判斷一段程式的結果是否符合預期。它提供了 `is` 這個巨集判斷結果是否爲真,以下是它的使用範例: 31 | 32 | ```clojure 33 | (is (= 4 (+ 2 2))) 34 | ;; => true 35 | (is (.startsWith "abcde" "ab")) 36 | ;; => true 37 | (is (instance? Integer 256)) 38 | ;; => FAIL in () (form-init542440103815122923.clj:1) 39 | ;; => expected: (instance? Integer 256) 40 | ;; => actual: java.lang.Long 41 | ;; => false 42 | ``` 43 | 44 | ### are 45 | 46 | 除了 `is` 之外,`clojure.test` 還提供了 `are` 這個巨集,幫助你將許多相似的判斷整理起來,使用方法如下: 47 | 48 | ```clojure 49 | (are [x y] (= x y) 50 | 4 (+ 2 2) 51 | 2 (+ 1 1)) 52 | ;; => true 53 | ``` 54 | 55 | 上面的範例跟以下的範例是一樣的,但是透過 `are` 就可以彙整衆多的 `is` 程式: 56 | 57 | ```clojure 58 | (is (= 4 (+ 2 2))) 59 | (is (= 2 (+ 1 1))) 60 | ``` 61 | 62 | ### thrown? 63 | 64 | 因爲 Clojure 是一個依靠 JVM 的語言,除了測試要執行的程式是否符合預期,也會有需要測試是否出現例外的時候。提供了 `thrown?` 巨集來完成這項工作: 65 | 66 | ```clojure 67 | (is (thrown? ArithmeticException (/ 1 0))) 68 | ``` 69 | 70 | ## 測試案例 71 | 72 | ### 撰寫 73 | 74 | 測試框架 `clojure.test` 提供 `deftest` 給使用者定義自己的測試案例,透過 `deftest` 中一個個寫好的判斷程式,來檢查欲執行的程式是否正確。 75 | 76 | ```clojure 77 | (deftest addition 78 | (is (= 4 (+ 2 2))) 79 | (is (= 7 (+ 3 4)))) 80 | ``` 81 | 82 | 不同的測試案例也可以再由另一個 `deftest` 包覆起來成爲一個更高階的測試案例。 83 | 84 | ```clojure 85 | (deftest arithmetic 86 | (addition) 87 | (subtraction)) 88 | ``` 89 | 90 | ### 說明文字 91 | 92 | 要清楚講明測試案例的作用,除了把命名儘量寫的容易理解之外,另一個方式就是在測試案例的說明註解中寫清楚。在 `clojure.test` 中,想利用文字清楚說明測試的意圖,可以在 `is` 中加上說明文字。當測試出錯時,該處的文字會出現在錯誤報告中: 93 | 94 | ```clojure 95 | (is (= 5 (+ 2 2)) "Crazy arithmetic") 96 | ;; => FAIL in () (form-init542440103815122923.clj:1) 97 | ;; => Crazy arithmetic 98 | ;; => expected: (= 5 (+ 2 2)) 99 | ;; => actual: (not (= 5 4)) 100 | ;; => false 101 | ``` 102 | 103 | 另外也提供了 `testing` 巨集,讓撰寫測試者可以將幾個斷言聚集在一起加上說明文字,不至於散亂而更有組織。同樣地,說明文字也會出現在錯誤報告中: 104 | 105 | ```clojure 106 | (deftest arithemetic-test 107 | (testing "Arithmetic" 108 | (testing "with positive integers" 109 | (is (= 4 (+ 2 2))) 110 | (is (= 7 (+ 3 4)))) 111 | (testing "with negative integers" 112 | (is (= -4 (+ -2 -2))) 113 | (is (= -1 (+ 3 -4)))))) 114 | ``` 115 | 116 | 要注意的是,`testing` 巨集只能在 `deftest` 中使用。 117 | 118 | ### 執行 119 | 120 | 寫好測試案例之後,可以透過 `clojure.test` 提供的 `run-tests` 來執行寫好的測試範例: 121 | 122 | ```clojure 123 | (run-tests 'your.namespace 'some.other.namespace) 124 | ``` 125 | 126 | 如果在 `run-tests` 中沒有寫下命名空間,將會執行目前命名空間中的測試案例。 127 | 128 | ```clojure 129 | (run-tests) 130 | ;; => 131 | ;; => Testing user 132 | ;; => 133 | ;; => Ran 2 tests containing 6 assertions. 134 | ;; => 0 failures, 0 errors. 135 | ;; => {:test 2, :pass 6, :fail 0, :error 0, :type :summary} 136 | ``` 137 | 138 | 或在命令列下使用 leiningen 執行測試: 139 | 140 | ```sh 141 | $ lein test 142 | 143 | lein test clj-test.core-test 144 | 145 | lein test :only clj-test.core-test/a-test 146 | 147 | FAIL in (a-test) (core_test.clj:7) 148 | FIXME, I fail. 149 | expected: (= 0 1) 150 | actual: (not (= 0 1)) 151 | 152 | Ran 1 tests containing 1 assertions. 153 | 1 failures, 0 errors. 154 | Tests failed. 155 | ``` 156 | 157 | ### 治具 158 | 159 | 有時候一些相關的測試案例執行之前,需要先啓動某些資源。比如資料庫的測試案例,就必須在所有測試開始之前,先與資料庫做好連線。在測試完畢之後,妥善地恢復成之前的樣貌。這種在測試案例之中的環境準備,就稱爲治具 (Fixture)。 160 | 161 | 在 `clojure.test` 中,Fixture 只是一個簡單的函式,它只接受一個參數。這個參數就是待執行的測試案例,如果想要在執行測試案例前後做一些準備或善後作業,只要在測試案例前後執行即可,範例如下: 162 | 163 | ```clojure 164 | (defn my-fixture [test-fn] 165 | ;; 在這裡設定或啓動必須事先準備好的事物 166 | (test-fn) ;; 呼叫測試案例 167 | ;; 在這裡做善後工作 168 | ) 169 | ``` 170 | 171 | Fixture 分爲兩種,一種是只需要執行一次,另一種是針對每個測試案例都會執行一次。以下是使用範例: 172 | 173 | ```clojure 174 | (use-fixtures :once load-data-fixture) ;; 只執行一次 175 | (use-fixtures :each add-test-id-fixture) ;; 每個測試案例都會執行一次 176 | ``` 177 | 178 | ## 回顧 179 | 180 | 經由本篇文章,你知道了如何引入 `clojure.test` 命名空間開始進行測試,也知道了幾種斷言可以用來檢驗運算式是否正確。知道了撰寫測試案例的方式,還有用說明文字輔助解釋測試案例。還知道了如何執行測試案例以及治具的使用方法。 181 | 182 | 還不賴吧?今天就先到這裡,下一篇文章再見囉! 183 | -------------------------------------------------------------------------------- /14.md: -------------------------------------------------------------------------------- 1 | # 下一步 2 | 3 | ## 回顧 4 | 5 | 你從一無所知,到現在對 Clojure 有了初步的認識。首先知道了 Clojure 的基本組成以及資料結構與型態,接着學會了如何建立繫結與函式,知道了控制程式流程的方式,以及如何組織專案和建立命名空間,以有系統的方式統整程式碼。 6 | 7 | 接下來你知道了自行建立資料型態的方式,並知道與 Java 和諧共存將會使你的程式更加穩健可靠。你還知道了在 Clojure 中讀取器扮演的角色,以及添加詮釋資料的手法。更重要的是,你學會了並行與併發的設計方法,以及如何有效正確的處理狀態。 8 | 9 | 你還學會了被稱爲魔法的巨集。接下來呢? 10 | 11 | 本篇文章將介紹延伸資訊,供讀者在讀完本系列文章後,進一步學習的方向和參考。 12 | 13 | ## 專案與函式庫 14 | 15 | ### ClojureScript 16 | 17 | [ClojureScript](https://goo.gl/bqVg9E) 是建立在 JavaScript 之上的 Clojure。使用了 Google 開發的 Closure Compiler 與 Closure Library,作爲轉譯工具與函式庫,將 Clojure 程式語言轉譯成 JavaScript。使用者可以使用已內建不變性與函數式設計的語言,而不需四處尋找解決方案或重新發明輪子。 18 | 19 | ### Spec 20 | 21 | 做爲一個動態語言,Clojure 雖然有類型註釋可以標註型態,但是因爲非強制,所以使用者必須更加小心型態之間的變化。 22 | 23 | 從 1.9 開始,Clojure 內建了 [clojure.spec](https://goo.gl/rMNAKU) 的命名空間,讓使用者可以建立型態與函式的規範。除了讓使用者清楚明瞭之外,還可以利用規則驗證資料、自動測試、以及自動產生測試資料。 24 | 25 | ### Cortex 26 | 27 | [Cortex](https://goo.gl/PAiBHC) 是一個以 Clojure 開發的機器學習函式庫,即將邁向 1.0 版本。 28 | 29 | ### Ring/Compojure 30 | 31 | [Ring](https://goo.gl/vSkKod) 類似與 Ruby 中的 Rack 或 Python 中的 WSGI,將 HTTP 抽象化後提供簡單的 API 供使用者建立網站應用程式。而 [Compojure](https://goo.gl/jzZGj2) 則是可以與 Ring 搭配使用的路由選擇函式庫。 32 | 33 | ### Reagent 34 | 35 | [Reagent](https://goo.gl/2F3RNo) 是由 ClojureScript 開發的框架,使用 Facebook 開發的 React 使用者介面函式庫,易於使用與簡潔的語法。 36 | 37 | ## 延伸閱讀 38 | 39 | 以下列出建議讀者繼續閱讀的書籍: 40 | 41 | - The Joy of Clojure,Manning 出版社 42 | 43 | - Mastering Clojure Macros,The Pragmatic Programmer 出版社 44 | 45 | - The Little Schemer,麻省理工出版社 46 | 47 | - On Lisp,作者 Paul Graham 48 | 49 | ## 本地社群 50 | 51 | 在本地,有一群熱愛 Lisp 與 Clojure 程式語言的有志之士,組織了 [Clojure Taiwan](https://clojure.tw/) 社群。每月的聚會除了有專題分享之外,還有定期的讀書會,與參與者一起學習 Clojure。 52 | 53 | 聯絡社群的管道有臉書、推特與 Telegram,詳細聯絡方式都可以在官網上找到。 54 | 55 | ## 關門之前 56 | 57 | 最後,本系列文章不應該是你學習 Clojure 的終點,而是起點。帶着從此處學習到的知識,踏進外頭廣闊的世界,將 Clojure 運用在你的工作或專案上,擁抱 Clojure。 58 | 59 | 將門關上並不是停止,而是踏出新的旅程。 60 | 61 | 還不賴吧?諸君,後會有期! 62 | 63 | > 每個人都知道他終會孤寂,在酒店關門之後。 64 | > 65 | > — 戴夫•凡•藍克《最後的召喚》 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wen-Chun Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 擁抱 Clojure 2 | 3 | 參加第九屆 iT 邦幫忙鐵人賽的系列文 4 | 5 | ## 目錄 6 | 7 | - [前言](https://github.com/cataska/embracing-clojure/blob/master/00.md) 8 | - [基本組成](https://github.com/cataska/embracing-clojure/blob/master/01.md) 9 | - [資料結構與型態](https://github.com/cataska/embracing-clojure/blob/master/02.md) 10 | - [繫結與函式](https://github.com/cataska/embracing-clojure/blob/master/03.md) 11 | - [流程控制](https://github.com/cataska/embracing-clojure/blob/master/04.md) 12 | - [命名空間與專案](https://github.com/cataska/embracing-clojure/blob/master/05.md) 13 | - [資料型別與協定](https://github.com/cataska/embracing-clojure/blob/master/06.md) 14 | - [與 Java 共舞](https://github.com/cataska/embracing-clojure/blob/master/07.md) 15 | - [讀取器與詮釋資料](https://github.com/cataska/embracing-clojure/blob/master/08.md) 16 | - [並行與併發](https://github.com/cataska/embracing-clojure/blob/master/09.md) 17 | - [巨集](https://github.com/cataska/embracing-clojure/blob/master/11.md) 18 | - [測試](https://github.com/cataska/embracing-clojure/blob/master/12.md) 19 | - [下一步](https://github.com/cataska/embracing-clojure/blob/master/14.md) --------------------------------------------------------------------------------