iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 8
0
Software Development

擁抱 Clojure系列 第 8

[第 08 天] 擁抱 Clojure:繫結與函式(二)

繫結與函式(二)

函式

多載

Java 中將類別裡擁有數個同樣名字的方法 (Method),參數個數卻不同稱爲多載 (Overloading),Clojure 也支援這種設計方法,同一個函式根據參數個數的不同,可以有不同的運算式,也可以呼叫同一個函式下不同參數個數的運算式:

(defn adder
  ([x] (adder x 10))
  ([x y] (+ x y)))
;; => #’user/adder
(adder 3)
;; => 13
(adder 3 2)
;; => 5

以上範例示範以一個參數和以兩個參數呼叫函式,由於根據參數而分開不同實作,所以結果並不相同。其中一個參數的版本還呼叫了自己的兩個參數的版本。

不定長度參數

有時候在定義函式的時候並不清楚參數的個數,或是想要接受不定個數的參數,Clojure 提供方法在定義函式時,聲明接受的是不定長度的參數。

在函式定義的參數向量中,在參數名稱前加上 & 符號與空白,則之後的參數都會包裝至序列之中:

(defn str-all-numbers [x & rest]
  (apply str "Hi " rest))
;; => #'user/str-all-numbers
(str-all-numbers 0 1 2 3 4)
;; => “Hi 1234”
(str-all-numbers 0)
;; => “Hi ”

在這個範例中,第一個呼叫 str-all-numbers 函式時代入了五個參數,x 爲第一個參數,其他參數則被裝進以 rest 命名的列表 (List) 中,由於只用到了後續的參數,所以第一個參數並不會被印出。

不定長度參數在 Clojure 中還具備了可選的屬性,亦即呼叫有不定長度參數的函式時,參數可以提供也可以不提供。第二次呼叫 str-all-numbers 函式時只給了第一個參數,後續的參數便不會被印出。

範例使用到的 apply 函式,第一個參數是函式,其後的參數將會被依序套用到第一個參數的函式中。

以下範例示範結合多載與不定長度參數的函式定義方法:

(defn overloading-variadic
  ([] 0)
  ([x] 1)
  ([x y] 2)
  ([x y & rest] "many arguments"))
;; => #'user/overloading-variadic
(overloading-variadic)
;; => 0
(overloading-variadic "one")
;; => 1
(overloading-variadic "one" "two")
;; => 2
(overloading-variadic "one" "two" "three")
;; => "many arguments"

解構

這裡有一個函式,接受一個列表當作參數,參數的作用是將列表中的第二與第四個位置相加:

(defn useless-adder [lst]
  (let [x (first (rest lst))
        y (first (rest (rest (rest lst))))]
    (+ x y)))
;; => #'user/useless-adder
(useless-adder [1 3 5 7 9])
;; => 10

看到層層 firstrest 是不是看到頭暈了呢?Clojure 提供了簡便的語法,可以更快速地取得參數的內容,稱爲解構 (Destructuring)。

向量解構

向量解構可以使用在任何序列型的群集,例如列表、向量、字串或序列。如同前一個例子,使用向量解構取得第二與第三個元素,會變得非常簡單:

(let [[_ x _ y] [1 3 5 7 9]]
  (+ x y))
;; => 10

這裡把不需要理會的元素以底線 (_) 來表示,需要取得的元素則賦予名字,根據擺放的位置匹配適當的元素。

巢狀向量中的元素也可以匹配:

(let [[_ _ [x y]] [1 2 [3 4] 5]]
  (* x y))
;; => 12

可以在解構式最後加上 :as 繫結整個待解構的向量:

(let [[x y :as original] [1 2 3 4 5]]
  (conj original (+ x y)))
;; => [1 2 3 4 5 3]

除了使用 :as 之外,還可以使用 & 來匹配其他未匹配的剩餘元素:

(let [[x & rest] [10 20 30 40 50]]
  rest)
;; => (20 30 40 50)

當然也可以將 :as& 兩個結合起來:

(let [[x & rest :as original] [2 4 6 8 10]]
  (println "x:" x ", rest:" rest ", original:" original))
;; => x: 2 , rest: (4 6 8 10) , original: [2 4 6 8 10]

以上的範例使用了 println 將資料輸出到螢幕,並加上換行。

有了解構之後,函式的參數就可以輕鬆地取得對應的內容:

(defn useful-adder [[_ x _ y]]
  (+ x y))
;; => #'user/useful-adder
(useful-adder [1 3 5 7 9])
;; => 10

跟一開始複雜的範例相比,是不是簡單很多呢。

映射解構

解構映射也跟解構向量一樣,根據擺放的位置與索引鍵,匹配對應的資料:

(def m {:a 5 :b 10 "c" 15})
;; => #'user/m
(let [{a :a b :b} m]
  (+ a b))
;; => 15

a 匹配索引鍵 :a 對應的資料、b 匹配索引鍵 :b 對應的資料。找不到對應的資料,會得到預設的 nil,如果不想使用預設的 nil,亦可以透過 :or 指定當某索引鍵找不到資料時,預設取得的資料:

(def m {:a 5 :b 10 "c" 15})
;; => #'user/m
(let [{a :a b :b d :d :or {d "OH"}} m]
  (println a b d))
;; => 5 10 OH

以上範例試圖取用索引鍵 :d 對應的資料,並在 :or 提供一個映射,指定了當找不到對應的資料時,應該選用的預設資料。

在向量解構時使用到的 :as 也可以使用在這裏,唯一不同的是不需要擺放在最後位置 (但建議還是放在最後):

(let [{a :a b :b :as whole} m]
  (println a b whole))
;; => 5 10 {:a 5, :b 10, c 15}

如果打算匹配的映射,其中的索引鍵都是由關鍵字組成的,Clojure 提供了 :keys 用來匹配映射中的關鍵字索引鍵。:keys 後加上一個向量,其中寫下打算匹配的關鍵字名稱,匹配後就可以透過名稱取得資料:

(def m {:a 10 :b 20 :c 15})
;; => #'user/m
(let [{:keys [a b]} m]
  (println a b))
;; => 10 20

如果映射中的索引鍵都是使用字串則使用 :strs、都是使用符號則用 :syms

(let [{:strs [a d]} {"a" "A", "b" "B", "c" "C", "d" "D"}]
  (println a d))
;; => A D
(let [{:syms [a d]} {'a "A", 'b "B", 'c "C", 'd "D"}]
  (println a d))
;; => A D

使用了映射解構的函式,就可以輕鬆地取出索引鍵代表的資料了:

(defn greet-user [{:keys [first-name last-name]}]
  (println "Welcome," first-name last-name))
;; => #'user/greet-user
(def catherine {:first-name "Catherine", :last-name "Chen", :age 40})
;; => #'user/catherine
(greet-user catherine)
;; => Welcome, Catherine Chen

(未完待續)


上一篇
[第 07 天] 擁抱 Clojure:繫結與函式(一)
下一篇
[第 09 天] 擁抱 Clojure:繫結與函式(三)
系列文
擁抱 Clojure30

尚未有邦友留言

立即登入留言