前言
作為一門函數語言程式設計語言,深入瞭解函式的定義和使用自然是十分重要的事情,下面我們一起來學習吧!
3種基礎定義方法
defn
定義語法
(defn name [params*]
exprs*)
示例
(defn tap [ns x]
(println ns x)
x)
fn
定義語法
(fn name? [params*]
exprs*)
示例
(def tap
(fn [ns x]
(println ns x)
x))
其實defn
是個macro,最終會展開為fn
這種定義方式。因此後面的均以fn
這種形式作說明。
Lambda表示式
定義語法
#(expr)
示例
(def tap
#(do
(println %1 %2)
%2))
注意:
- Lambda表示式的函式體只允許使用一個表示式,因此要通過special form
do
來執行多個表示式; - 入參symbol為
%1,%2,...%n
,當有且只有一個入參時可以使用%
來指向該入參。
Metadata——為函式附加後設資料
Symbol和集合均支援附加metadata,以便向編譯器提供額外資訊(如型別提示等),而我們也可以通過metadata來標記原始碼、訪問策略等資訊。
對於命名函式我們自然要賦予它Symbol,自然就可以附加後設資料了。
其中附加:private
和defn-
定義函式目的是一樣的,就是將函式的訪問控制設定為private(預設為public),但可惜的是cljs現在還不支援:private
,所以還是要用名稱來區分訪問控制策略。
示例:
;; 定義
(defn
^{:doc "my sum function"
:test (fn []
(assert (= 12 (mysum 10 1 1))))
:custom/metadata "have nice time!"}
mysum [& xs]
(apply + xs))
;; 獲取Var的metadata
(meta #'mysum)
;;=>
;; {:name mysum
;; :custom/metadata "have nice time!"
;; :doc "my sum function"
;; :arglists ([& xs])
;; :file "test"
;; :line 126
;; :ns #<Namespace user>
;; :test #<user$fn_289 user$fn_289@20f443>}
若只打算設定document string而已,那麼可以簡寫為
(defn mysum
"my sum function"
[& xs]
(apply + xs))
雖然cljs只支援:doc
根據入引數目實現函式過載(Multi-arity Functions)
示例
(fn tap
([ns] (tap ns nil))
([ns x] (println ns x))
([ns x & more] (println ns x more)))
引數解構
cljs為我們提供強大無比的入參解構能力,也就是通過宣告方式萃取入參
基於位置的解構(Positional Destructuring)
;; 定義1
(def currency-of
(fn [[amount currency]]
(println amount currency)
amount))
;; 使用1
(currency-of [12 "US"])
;; 定義2
(def currency-of
(fn [[amount currency [region ratio]]]
(println amount currency region ratio)
amount))
;; 使用2
(currency-of [12 "US" ["CHINA" 6.7]])
鍵值對的解構(Map Destructuring)
;; 定義1,鍵型別為Keyword
(def currency-of
(fn [{currency :curr}]
(println currency)))
;; 使用1
(currency-of {:curr "US"})
;; 定義2,鍵型別為String
(def currency-of
(fn [{currency "curr"}]
(println currency)))
;; 使用2
(currency-of {"curr" "US"})
;; 定義3,鍵型別為Symbol
(def currency-of
(fn [{currency 'curr}]
(println currency)))
;; 使用3
(currency-of {'curr "US"})
;; 定義4,一次指定多個鍵
(def currency-of
(fn [{:keys [currency amount]}]
(println currency amount)))
;; 使用4
(currency-of {:currency "US", :amount 12})
;; 定義5,一次指定多個鍵
(def currency-of
(fn [{:strs [currency amount]}]
(println currency amount)))
;; 使用5
(currency-of {"currency" "US", "amount" 12})
;; 定義6,一次指定多個鍵
(def currency-of
(fn [{:syms [currency amount]}]
(println currency amount)))
;; 使用6
(currency-of {'currency "US", 'amount 12})
;; 定義7,預設值
(def currency-of
(fn [{:keys [currency amount] :or {currency "CHINA"}}]
(println currency amount)))
;; 使用7
(currency-of {:amount 100}) ;;=> 100CHINA
;; 定義8,命名鍵值對
(def currency-of
(fn [{:keys [currency amount] :as orig}]
(println (:currency orig))))
(currency-of {'currency "US", 'amount 12}) ;;=> US
可變入參(Variadic Functions)
通過&
定義可變入參,可變入參僅能作為最後一個入參來使用
(def tap
(fn [ns & more]
(println ns (first more))))
(tap "user.core" "1" "2" "3") ;;=> user.core1
命名入參(Named Parameters/Extra Arguments)
通過組合可變入參和引數解構,我們可以得到命名入參
(def tap
(fn [& {:keys [ns msg] :or {msg "/nothing"}}]
(println ns msg)))
(tap :ns "user.core" :msg "/ok") ;;=> user.core/ok
(tap :ns "user.core") ;;=> user.core/nothing
Multimethods
Multi-Arity函式中我們可以通過入引數目來呼叫不同的函式實現,但有沒有一種如C#、Java那樣根據入參型別來呼叫不同的函式實現呢?clj/cljs為我們提供Multimethods這一殺技——不但可以根據型別呼叫不同的函式實現,還可以根據以下內容呢!
- 型別
- 值
- 屬性
- 後設資料
- 入參間關係
想說"Talk is cheap, show me the code"嗎?在看程式碼前,我們先看看到底Multimethods的組成吧
1.dispatching function
用於對函式入參作操作,如獲取型別、值、運算入參關係等,然後將返回值作為dispatching value,然後根據dispatching value呼叫具體的函式實現。
;; 定義dispatching function
(defmulti name docstring? attr-map? dispatch-fn & options)
;; 其中options是鍵值對
;; :default :default,指定預設dispatch value的值,預設為:default
;; :hierarchy {},指定使用的hierarchy object
2.method
具體函式實現
;; 定義和註冊新的函式到multimethod
(defmethod multifn dispatch-val & fn-tail)
3.hierarchy object
儲存層級關係的物件,預設情況下所有相關的Macro和函式均採用全域性hierarchy object,若要採用私有則需要通過(make-hierarchy)
來建立。
還是一頭霧水?上示例吧!
示例1 —— 根據第二個入參的層級關係
(defmulti area
(fn [x y]
y))
(defmethod area ::a
[x y] (println "derive from ::a"))
(defmethod area :default
[x y] (println "executed :default"))
(area 1 `a) ;;=> executed :default
(derive `a :a)
(area 1 `a) ;;=>derive from ::a
示例2 -- 根據第一個入參的值
(defmulti area
(fn [x y]
x))
(defmethod area 1
[x y] (println "x is 1"))
(defmethod area :default
[x y] (println "executed :default"))
(area 2 `a) ;;=> executed :default
(area 1 :b) ;;=> x is 1
示例3 -- 根據兩入引數值比較的大小
(defmulti area
(fn [x y]
(> x y)))
(defmethod area true
[x y] (println "x > y"))
(defmethod area :default
[x y] (println "executed :default"))
(area 1 2) ;;=> executed :default
(area 2 3) ;;=> x > y
刪除method
;; 函式簽名
(remove-method multifn dispatch-val)
;; 示例
(remove-method area true)
分發規則
先對dispatching value和method的dispatching-value進行=
的等於操作,若不匹配則對兩者進行isa?
的層級關係判斷操作,就這樣遍歷所有註冊到該multimethod的method,得到一組符合的method。若這組method的元素個數有且僅有一個,則執行該method;若沒有則執行:default
method,若還是沒有則拋異常。若這組method的元素個數大於1,且沒有人工設定優先順序,則拋異常。
通過prefer-method
我們可以設定method的優先順序
(derive `a `b)
(derive `c `a)
(defmulti test
(fn [x] (x)))
(defmethod test `a
[x] (println "`a"))
(defmethod test `b
[x] (println "`b"))
;; (test `c) 這裡就不會出現多個匹配的method
(prefer-method `a `b)
(test `c) ;;=> `a
層級關係
層級關係相關的函式如下:
;; 判斷層級關係
(isa? h? child parent)
;; 構造層級關係
(derive h? child parent)
;; 解除層級關係
(underive h? child parent)
;; 構造區域性hierarchy object
(make-hierarchy)
上述函式當省略h?
時,則操作的層級關係儲存在全域性的hierarchy object中。
注意:層級關係儲存在全域性的hierarchy object中時,Symbole、Keyword均要包含名稱空間部分(即使這個名稱空間並不存在),否則會拒絕。
(ns cljs.user)
;; Symbole, `b會展開為cljs.user/b
(derive 'dummy/a `b)
;; Keyword, ::a會展開為cljs.user/:a
(derive ::a ::b)
另外還有parent
、ancestors
和descendants
(derive `c `p)
(derive `p `pp)
;; 獲取父層級
(parent `c) ;;=> `p
;; 獲取祖先
(ancestors `c) ;;=> #{`p `pp}
;; 獲取子孫
(descendants `pp) ;;=> #{`p `c}
區域性層級關係
通過(make-hierarchy)
可以建立一個用於實現區域性層級關係的hierarchy object
(def h (make-hierarchy))
(def h (derive h 'a 'b))
(def h (derive h :a :b))
(isa? h 'a 'b)
(isa? h :a :b)
注意:區域性層級關係中的Symbol和Keyword是可以包含也可以不包含名稱空間部分的哦!
Condition Map
對於動態型別語言而言,當入參不符合函式定義所期待時,是將入參格式化為符合期待值,還是直接報錯呢?我想這是每個JS的工程師必定面對過的問題。面對這個問題我們應該分階段分模組來處理。
- 開發階段,對於核心模組,讓問題儘早暴露;
- 生產階段,對於與使用者互動的模組,應格式化輸入,並在後臺記錄跟蹤問題。
而clj/cljs函式中的condition map
就是為我們在開發階段提供對函式入參、函式返回值合法性的斷言能力,讓我們儘早發現問題。
(fn name [params*] condition-map? exprs*)
(fn name ([params*] condition-map? exprs*)+)
; condition-map? => {:pre [pre-exprs*]
; :post [post-exprs*]}
; pre-exprs 就是作為一組對入參的斷言
; post-exprs 就是作為一組對返回值的斷言
示例
(def mysum
(fn [x y]
{:pre [(pos? x) (neg? y)]
:post [(not (neg? %))]}
(+ x y)))
(mysum 1 1) ;; AssertionError Assert failed: (neg? y) user/mysum
(mysum -1 1) ;; AssertionError Assert failed: (pos? x) user/mysum
(mysum 1 -2) ;; AssertionError Assert failed: not (neg? %)) user/mysum
在pre-exprs中我們可以直接指向函式的入參,在post-exprs中則通過%
來指向函式的返回值。
雖然增加函式執行的前提條件,而且可以針對函式的值、關係、後設資料等進行合法性驗證,但依舊需要在執行時才能觸發驗證(這些不是執行時才觸發還能什麼時候能觸發呢?)。對動態型別語言天然編譯期資料型別驗證,我們可以通過core.typed這個專案去增強哦!
總結
現在我們可以安心把玩函式了,oh yeah!
尊重原創,轉載請註明來自:http://www.cnblogs.com/fsjohnhuang/p/7137597.html ^_^肥仔John