Clojure

學習 Clojure - 函式

建立函式

Clojure 是一種函式語言。函式是一等公民,可以傳遞給其他函式或從其他函式傳回。大部分 Clojure 程式碼主要由純函式組成(沒有副作用),因此使用相同的輸入會產生相同的輸出。

defn 定義一個命名函式

;;    name   params         body
;;    -----  ------  -------------------
(defn greet  [name]  (str "Hello, " name) )

此函式有一個單一參數 name,但你可以在 params 向量中包含任意數量的參數。

在「函式位置」(清單的第一個元素)呼叫函式,函式名稱在「函式位置」中

user=> (greet "students")
"Hello, students"

多重參數函式

可以定義函式來取得不同數量的參數(不同的「參數數」)。不同的參數數必須全部在同一個 defn 中定義 - 使用 defn 超過一次會取代先前的函式。

每個參數數都是一個清單 ([param*] body*)。一個參數數可以呼叫另一個參數數。主體可以包含任意數量的表達式,而回傳值是最後一個表達式的結果。

(defn messenger
  ([]     (messenger "Hello world!"))
  ([msg]  (println msg)))

此函式宣告兩個參數數(0 個參數和 1 個參數)。0 個參數的參數數會呼叫 1 個參數的參數數,並使用預設值來列印。我們透過傳遞適當數量的引數來呼叫這些函式

user=> (messenger)
Hello world!
nil

user=> (messenger "Hello class!")
Hello class!
nil

可變參數函式

函數也可以定義變數數量的參數 - 這稱為「可變參數」函數。變數參數必須出現在參數清單的結尾。它們將被收集在一個序列中,供函數使用。

變數參數的開頭以 & 標記。

(defn hello [greeting & who]
  (println greeting who))

此函數採用參數 greeting 和變數數量的參數 (0 個或更多),這些參數將收集在名為 who 的清單中。我們可以透過使用 3 個參數來呼叫它,來看到這一點

user=> (hello "Hello" "world" "class")
Hello (world class)

您會看到,當 println 列印 who 時,它會列印為收集的兩個元素的清單。

匿名函數

可以使用 fn 建立匿名函數

;;    params         body
;;   ---------  -----------------
(fn  [message]  (println message) )

由於匿名函數沒有名稱,因此無法在稍後參照它。相反,匿名函數通常在傳遞給其他函數時建立。

或者可以立即呼叫它 (這不是常見用法)

;;     operation (function)             argument
;; --------------------------------  --------------
(  (fn [message] (println message))  "Hello world!" )

;; Hello world!

在這裡,我們在立即使用參數呼叫表達式的較大表達式的函數位置中定義了匿名函數。

許多語言都有陳述式,這些陳述式強制執行某些操作而不傳回值,以及這樣做的表達式。Clojure 有傳回值的表達式。我們稍後會看到,這甚至包括像 if 這樣的流程控制表達式。

defnfn

defn 視為 deffn 的縮寫會很有用。fn 定義函數,而 def 將其繫結到名稱。這些是等效的

(defn greet [name] (str "Hello, " name))

(def greet (fn [name] (str "Hello, " name)))

匿名函數語法

在 Clojure 讀取器中實作的 fn 匿名函數語法有一個較短的形式:#()。此語法省略參數清單,並根據參數的位置命名參數。

  • % 用於單一參數

  • %1%2%3 等用於多個參數

  • %& 用於任何剩餘的 (可變參數) 參數

巢狀匿名函數會造成歧義,因為參數沒有命名,因此不允許巢狀。

;; Equivalent to: (fn [x] (+ 6 x))
#(+ 6 %)

;; Equivalent to: (fn [x y] (+ x y))
#(+ %1 %2)

;; Equivalent to: (fn [x y & zs] (println x y zs))
#(println %1 %2 %&)

陷阱

一個常見的需求是採用元素並將其包裝在向量中的匿名函數。您可能會嘗試寫成

;; DO NOT DO THIS
#([%])

此匿名函數擴充為等效的

(fn [x] ([x]))

此表單會包裝在一個向量中嘗試呼叫沒有參數的向量(額外的括號對)。相反

;; Instead do this:
#(vector %)

;; or this:
(fn [x] [x])

;; or most simply just the vector function itself:
vector

套用函數

apply

apply 函數會呼叫具有 0 個或更多固定參數的函數,並從最後一個序列中擷取其餘需要的參數。最後一個參數必須是一個序列。

(apply f '(1 2 3 4))    ;; same as  (f 1 2 3 4)
(apply f 1 '(2 3 4))    ;; same as  (f 1 2 3 4)
(apply f 1 2 '(3 4))    ;; same as  (f 1 2 3 4)
(apply f 1 2 3 '(4))    ;; same as  (f 1 2 3 4)

這 4 個呼叫都等於 (f 1 2 3 4)。當參數以序列形式傳遞給你,但你必須使用序列中的值呼叫函數時,apply 會很有用。

例如,你可以使用 apply 來避免撰寫此內容

(defn plot [shape coords]   ;; coords is [x y]
  (plotxy shape (first coords) (second coords)))

相反,你可以簡單地撰寫

(defn plot [shape coords]
  (apply plotxy shape coords))

區域變數和封閉

let

let 在「詞彙範圍」中將符號繫結到值。詞彙範圍會為名稱建立一個新的內容,並嵌套在周圍的內容中。在 let 中定義的名稱優先於外部內容中的名稱。

;;      bindings     name is defined here
;;    ------------  ----------------------
(let  [name value]  (code that uses name))

每個 let 都可以定義 0 個或更多繫結,並且可以在主體中包含 0 個或更多表達式。

(let [x 1
      y 2]
  (+ x y))

let 表達式為 xy 建立兩個區域繫結。表達式 (+ x y) 位於 let 的詞彙範圍中,並將 x 解析為 1,將 y 解析為 2。在 let 表達式之外,x 和 y 將沒有持續的意義,除非它們已經繫結到一個值。

(defn messenger [msg]
  (let [a 7
        b 5
        c (clojure.string/capitalize msg)]
    (println a b c)
  ) ;; end of let scope
) ;; end of function

messenger 函數會採用 msg 參數。在這裡,defn 也會為 msg 建立詞彙範圍 - 它只在 messenger 函數中具有意義。

在該函數範圍內,let 會建立一個新的範圍來定義 abc。如果我們嘗試在 let 表達式之後使用 a,編譯器會報告錯誤。

封閉

fn 特殊形式會建立一個「封閉」。它會「封閉」周圍的詞彙範圍(例如上述的 msgabc),並擷取它們在詞彙範圍之外的值。

(defn messenger-builder [greeting]
  (fn [who] (println greeting who))) ; closes over greeting

;; greeting provided here, then goes out of scope
(def hello-er (messenger-builder "Hello"))

;; greeting value still available because hello-er is a closure
(hello-er "world!")
;; Hello world!

Java 互操作

呼叫 Java 程式碼

以下是從 Clojure 呼叫 Java 的呼叫慣例摘要

任務 Java Clojure

實例化

new Widget("foo")

(Widget. "foo")

實例方法

rnd.nextInt()

(.nextInt rnd)

實例欄位

object.field

(.-field object)

靜態方法

Math.sqrt(25)

(Math/sqrt 25)

靜態欄位

Math.PI

Math/PI

Java 方法與函式

  • Java 方法並非 Clojure 函式

  • 無法儲存或傳遞為引數

  • 必要時可將其包裝在函式中

;; make a function to invoke .length on arg
(fn [obj] (.length obj))

;; same thing
#(.length %)

測試您的知識

1) 定義一個不帶引數並列印「Hello」的函式 greet。用實作取代 ___(defn greet [] _)

2) 使用 def 重新定義 greet,首先使用 fn 特殊形式,然後使用 #() 讀取巨集。

;; using fn
(def greet __)

;; using #()
(def greet __)

3) 定義一個函式 greeting,其

  • 未給定引數,傳回「Hello, World!」

  • 給定一個引數 x,傳回「Hello, x!」

  • 給定兩個引數 x 和 y,傳回「x, y!」

;; Hint use the str function to concatenate strings
(doc str)

(defn greeting ___)

;; For testing
(assert (= "Hello, World!" (greeting)))
(assert (= "Hello, Clojure!" (greeting "Clojure")))
(assert (= "Good morning, Clojure!" (greeting "Good morning" "Clojure")))

4) 定義一個函式 do-nothing,其帶一個單一引數 x 並傳回不變更的 x。

(defn do-nothing [x] ___)

在 Clojure 中,這是 identity 函式。identity 本身並非非常有用,但在處理高階函式時有時是必要的。

(source identity)

5) 定義一個函式 always-thing,其帶任意數量的引數,忽略所有引數並傳回數字 100

(defn always-thing [__] ___)

6) 定義一個函式 make-thingy,其帶一個單一引數 x。它應該傳回另一個函式,該函式帶任意數量的引數並總是傳回 x。

(defn make-thingy [x] ___)

;; Tests
(let [n (rand-int Integer/MAX_VALUE)
      f (make-thingy n)]
  (assert (= n (f)))
  (assert (= n (f 123)))
  (assert (= n (apply f 123 (range)))))

在 Clojure 中,這是 constantly 函式。

(source constantly)

7) 定義一個函式 triplicate,其帶另一個函式並在沒有任何引數的情況下呼叫它三次。

(defn triplicate [f] ___)

8) 定義一個函式 opposite,其帶一個單一引數 f。它應該傳回另一個函式,該函式帶任意數量的引數,對其套用 f,然後對結果呼叫 not。Clojure 中的 not 函式執行邏輯否定。

(defn opposite [f]
  (fn [& args] ___))

在 Clojure 中,這是 complement 函式。

(defn complement
  "Takes a fn f and returns a fn that takes the same arguments as f,
  has the same effects, if any, and returns the opposite truth value."
  [f]
  (fn
    ([] (not (f)))
    ([x] (not (f x)))
    ([x y] (not (f x y)))
    ([x y & zs] (not (apply f x y zs)))))

9) 定義一個函式 triplicate2,其帶另一個函式和任意數量的引數,然後對這些引數呼叫該函式三次。重新使用您在先前的 triplicate 練習中定義的函式。

(defn triplicate2 [f & args]
  (triplicate ___))

10) 使用 java.lang.Math 類別 (Math/powMath/cosMath/sinMath/PI) 說明下列數學事實

  • pi 的餘弦值為 -1

  • 對於某些 x,sin(x)^2 + cos(x)^2 = 1

11) 定義一個函數,它將 HTTP URL 作為字串,從網路擷取該 URL,並將內容作為字串傳回。

提示:使用 java.net.URL 類別及其 openStream 方法。然後使用 Clojure slurp 函數將內容作為字串取得。

(defn http-get [url]
  ___)

(assert (.contains (http-get "https://www.w3.org") "html"))

事實上,Clojure slurp 函數會先將其引數解釋為 URL,然後再嘗試將其作為檔案名稱。撰寫一個簡化的 http-get

(defn http-get [url]
  ___)

12) 定義一個函數 one-less-arg,它採用兩個引數

  • f,一個函數

  • x,一個值

並傳回另一個函數,它對 x 加上任何其他引數來呼叫 f

(defn one-less-arg [f x]
  (fn [& args] ___))

在 Clojure 中,partial 函數是此函數的較一般版本。

13) 定義一個函數 two-fns,它採用兩個函數作為引數,fg。它傳回另一個函數,它採用一個引數,對其呼叫 g,然後對結果呼叫 f,並傳回該結果。

也就是說,您的函數傳回 fg 的組合。

(defn two-fns [f g]
  ___)