前言

?作为一门函数式编程语言,深入了解函数的定义和使用自然是十分重要的事情,下面我们一起来学习吧!

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

http://www.cnblogs.com/fsjohnhuang/p/7137597.html