Programming clojure – Multimethods

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#))))
参数类型的重载, 这对于弱类型语言比较困难,
对于clojure需要使用Multimethods(Dispatch by class)来实现
对于python实现起来更麻烦一些,
这是Guido van Rossum实现的python版的Multimethods,
http://www.artima.com/weblogs/viewpost.jsp?thread=101605

当然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

Living Without Multimethods

给个例子, 我们实现一个可以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, 并且如果类型越来越多, 代码的清晰和维护都是问题

 

Defining Multimethods

如何定义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)

 

Dispatch by Class

上面的例子, 就可以简单的写成这样, 解决了和函数重载同样的问题

(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* ">" ))

Dispatch Is Inheritance-Aware

Clojure是基于Java的, 所以处处参杂着oo的痕迹...

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

Moving Beyond Simple Dispatch

Dispatch by class会有一个问题, 就是多重继承
当Dispatch同时匹配到两个defmethod的时候怎么办?

例子,

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

 

Creating Ad Hoc Taxonomies

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)

Adding Inheritance to Ad Hoc Types

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)

个人觉得这个方法其实不是很好, 其实可以实现机制直接忽略第二个条件.

 

When Should I Use Multimethods?

文中说了很多,

首先Multimethods在Clojure中被使用的并不多, 尤其是by ad hoc type, 更少

我个人觉得, 没那么绝对, 这个机制不是用来实现简单的if-else替换或函数重载的, 而且使用起来并不方便

所以, 当你真正需要的时候, 你愿意为使用它付出代码繁琐的代价时, 那就是你应该使用Multimethods的时候...

你可能感兴趣的:(programming)