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 25servicecharge.Normalsavingsaccountspaya25servicecharge.•Normalsavingsaccountspaya10 service charge. 
• Premium accounts have no fee. 
• Checking accounts with a balance of 5,000ormorearepremium.Savingsaccountswithabalanceof5,000ormorearepremium.•Savingsaccountswithabalanceof1,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的时候...



本文章摘自博客园,原文发布日期: 2013-02-28

目录
相关文章
|
7月前
|
算法 安全 编译器
【C++20 新特性Concepts 概念】C++20 Concepts: Unleashing the Power of Template Programming
【C++20 新特性Concepts 概念】C++20 Concepts: Unleashing the Power of Template Programming
309 0
|
Java Scala Android开发
What is the future of Kotlin programming language?
Elina Bessarabova, works at Mind StudiosAnswered Mar 31 Good afternoon here, and thank you for an interesting question to ask...
1329 0

热门文章

最新文章