Multimethods, 其实就是FP基础里面说的, Pattern Matching, 说白了, 就是根据不同的参数定义不同的逻辑.
我首先想到的是函数重载, http://www.cnblogs.com/skynet/archive/2010/09/05/1818636.html
参数个数重载, 对于这种clojure函数天然支持, 如下可以定义多组参数列表
(defmacro and ([] true) ([x] x) ([x & rest] `(let [and# ~x] (if and# (and ~@rest) and#))))
当然Multimethods不是仅仅函数重载那么简单,
Multimethods is similar to Java polymorphism but more general
Polymorphism is the ability of a function or method to have different definitions depending on the type of the target object.
Multimethods不但可以做到这点, 还可以更加general, 比如通过对值(或多个条件综合)的判断选择不同的逻辑(Dispatch By Ad Hoc Type)
多态或面向对象解决什么问题?
用更高的抽象来解决面向过程代码的杂乱和臃肿
而造成这种杂乱的原因很大一部分是由于大量的不断变化的if-else...
面向对象将分支逻辑封装在大量的类中, 但仍然无法避免if-else, 因为没有封装判断条件
你仍然需要在不同的条件下调用不同类的function, 或使用多态, 给基类指针绑定不同的子类对象
比如工厂模式, 你仍然需要在不同的情况下创建不同的工厂类
可以使用eval(python和clojure都有)部分的解决这个问题, 其实就是将判断条件封装在类名中
所以现在比较清晰的是, Multimethods其实也在解决这个问题, 并解决的更好
他不需要使用比较重量级的(高overhead)类来解决这样的问题, 而可以直接使用函数.
并且很好的封装的判断条件, 可以自动的根据判断条件选择适合的function
给个例子, 我们实现一个可以print不同类型数据的函数my-print
由于不同类型print的逻辑不一样, 所以需要if-else
(defn my-print [ob] (cond (vector? ob) (my-print-vector ob) ;为了使例子清楚,不列出my-print-vector的具体实现 (nil? ob) (.write *out* "nil") (string? ob) (.write *out* ob)))
这样的问题是不好维护, 每次支持新的类型, 都需要修改my-print, 并且如果类型越来越多, 代码的清晰和维护都是问题
如何定义multimethod, 分两步, 感觉不太好解释
如果你想象成switch…case, defmulti中的dispatch-fn其实就是switch中的计算逻辑
而defmethod中的dispatch-val就是case中的value
To define a multimethod, use defmulti:
(defmulti name dispatch-fn)
To add a specific method implementation to my-println, use defmethod:
(defmethod name dispatch-val & fn-tail)
上面的例子, 就可以简单的写成这样, 解决了和函数重载同样的问题
(defmulti my-print class) ;switch (class(s)) (defmethod my-print String [s] ; case: String (.write *out* s)) (defmethod my-print nil [s] ;case: nil (.write *out* "nil" )) (defmethod my-print vector [s] (my-print-vector s))
(defmethod my-print :default [s] ;switch…case也需要default (.write *out* "#<" ) (.write *out* (.toString s)) (.write *out* ">" ))
Multimethod dispatch knows about Java inheritance.
(defmethod my-print Number [n] (.write *out* (.toString n))) (my-println 42) ;不会报错:int不是number 42
42 is an Integer, not a Number. Multimethod dispatch is smart enough to know that an integer is a number and match anyway.
(isa? Integer Number) true
例子,
(defmethod my-print java.util.Collection [c] (.write *out* "(") (.write *out* (str-join " " c)) (.write *out* ")")) (defmethod my-print clojure.lang.IPersistentVector [c] ;显示Vector特殊格式 (.write *out* "[") (.write *out* (str-join " " c)) (.write *out* "]"))
如下调用就会报错, 原因vector是多重继承自Collection和IPersistentVector
(my-println [1 2 3])
java.lang.IllegalArgumentException: Multiple methods match dispatch value:
class clojure.lang.LazilyPersistentVector –> interface clojure.lang.IPersistentVector and interface java.util.Collection,
and neither is preferred
Clojure的解决办法就是, 通过perfer-method来指定preferred关系
Many languages constrain method dispatch to make sure these conflicts never happen, such as by forbidding multiple
inheritance. Clojure takes a different approach. You can create conflicts, and you can resolve them with prefer-method:
(prefer-method multi-name loved-dispatch dissed-dispatch)
(prefer-method
my-print clojure.lang.IPersistentVector java.util.Collection)
Multimethods强大的地方就是不但可以Dispatch by class, 还可以Dispatch by Ad Hoc type
例子, 定义银行帐号, tag分为checking(活期), saving(定期), balance为余额
(ns examples.multimethods.account) (defstruct account :id :tag :balance)
在当前namespace定义两个keyword
::Checking :examples.multimethods.account/Checking ::Savings :examples.multimethods.account/Savings
The capital names are a Clojure conventionto show the keywords are acting as types.
The doubled :: causes the keywords to resolve in the current namespace.
为了便于使用, 定义命名空间缩写,
(alias 'acc 'examples.multimethods.account)
下面定义一个简单的计算利率应用, 可以通过参数值来决定逻辑
(defmulti interest-rate :tag) (defmethod interest-rate ::acc/Checking [_] 0M) (defmethod interest-rate ::acc/Savings [_] 0.05M)
再实现一个比较复杂的计算年费的应用, 更可以看出Multimethods的强大
• Normal checking accounts pay a $25 service charge.
• Normal savings accounts pay a $10 service charge.
• Premium accounts have no fee.
• Checking accounts with a balance of $5,000 or more are premium.
• Savings accounts with a balance of $1,000 or more are premium.
活期和储蓄账户收取年费的门槛和费用都是不同的
先实现是否需要缴费的function, 仍然是通过value来选择逻辑
(defmulti account-level :tag) (defmethod account-level ::acc/Checking [acct] (if (>= (:balance acct) 5000) ::acc/Premium ::acc/Basic)) (defmethod account-level ::acc/Savings [acct] (if (>= (:balance acct) 1000) ::acc/Premium ::acc/Basic))
再实现年费function, 这个需要同时根据tag类型和account-level两个条件来决定
Multimethods可以组合判断多个条件, 非常强大,
(defmulti service-charge (fn [acct] [(account-level acct) (:tag acct)])) (defmethod service-charge [::acc/Basic ::acc/Checking] [_] 25) (defmethod service-charge [::acc/Basic ::acc/Savings] [_] 10) (defmethod service-charge [::acc/Premium ::acc/Checking] [_] 0) (defmethod service-charge [::acc/Premium ::acc/Savings] [_] 0)
There is one further improvement you can make to service-charge.
还可以做的一步优化是, 可以将最后两个defmethod合并成一个, 因为其实只要是::acc/Premium, 结果都是0
采用的方法是,
Clojure lets you define arbitrary parent/child relationships with derive:
(derive child parent)
(derive ::acc/Savings ::acc/Account) (derive ::acc/Checking ::acc/Account) (defmethod service-charge [::acc/Premium ::acc/Account] [_] 0)
个人觉得这个方法其实不是很好, 其实可以实现机制直接忽略第二个条件.
文中说了很多,
首先Multimethods在Clojure中被使用的并不多, 尤其是by ad hoc type, 更少
我个人觉得, 没那么绝对, 这个机制不是用来实现简单的if-else替换或函数重载的, 而且使用起来并不方便
所以, 当你真正需要的时候, 你愿意为使用它付出代码繁琐的代价时, 那就是你应该使用Multimethods的时候...