文章出處

前言

 作為一門函數式編程語言,深入了解函數的定義和使用自然是十分重要的事情,下面我們一起來學習吧!

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))

注意:

  1. Lambda表達式的函數體只允許使用一個表達式,因此要通過special formdo來運行多個表達式;
  2. 入參symbol為%1,%2,...%n,當有且只有一個入參時可以使用%來指向該入參。

Metadata——為函數附加元數據

 Symbol和集合均支持附加metadata,以便向編譯器提供額外信息(如類型提示等),而我們也可以通過metadata來標記源碼、訪問策略等信息。
 對于命名函數我們自然要賦予它Symbol,自然就可以附加元數據了。
 其中附加:privatedefn-定義函數目的是一樣的,就是將函數的訪問控制設置為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這一殺技——不但可以根據類型調用不同的函數實現,還可以根據以下內容呢!

  1. 類型
  2. 屬性
  3. 元數據
  4. 入參間關系

 想說"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)

另外還有parentancestorsdescendants

(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的工程師必定面對過的問題。面對這個問題我們應該分階段分模塊來處理。

  1. 開發階段,對于內核模塊,讓問題盡早暴露;
  2. 生產階段,對于與用戶交互的模塊,應格式化輸入,并在后臺記錄跟蹤問題。
     而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


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()