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

时间: 2024-09-10 15:06:09

Programming clojure – Multimethods的相关文章

Programming clojure – Recursion and Lazy-seq

5.1 Functional Programming Concepts The Six Rules Although the benefits of FP are compelling, FP is a wholesale change from the imperative programming style that dominates much of the programming world today. Plus, Clojure takes a unique approach to

Programming Clojure - Unifying Data with Sequences

In Clojure, all these data structures can be accessed through a single abstraction: the sequence (or seq).  A seq (pronounced "seek") is a logical list, the seq is an abstraction that can be used everywhere.   Collections that can be viewed as s

Programming clojure – Concurrency

Clojure的并发, 这兄弟写的比较系统, http://www.blogjava.net/killme2008/archive/2010/07/archive/2010/07/14/326027.html   Concurrency is a fact of life and, increasingly, a fact of software. 为什么需要并发? • Expensive computations may need to execute in parallel on multi

Clojure - 基本语法

Installing Clojure Clojure is an open-source project hosted at github.com. git clone https://github.com/clojure/clojure.git This will download the code from the master branch into the clojure directory in your workspace. Clojure is a Java project, an

RDBMS vs. NoSQL &amp; Clojure概述

RDBMS vs. NoSQL 合作还是竞争 数据库要解决的主要问题 不管是RDBMS还是NoSQL,在大的方面他们都属于数据库这个范畴,这个范畴之内所要面临的一些共同问题有哪些呢.下面的图是一个大致的归纳. 从图中可以看出,一个数据库系统主要解决以下几个问题: 数据的存储,即要存入哪些数据到系统中,当然在data definition这一块,有schema和no schema两种,说白了就是数据格式和数据关系的定义问题 完成了data definition,那么接下来自然要发生的事情就是将数据

技术书单整理

算法 算法导论 Introduction to Algorithms, Second Edition, by Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein 算法概论  Algorithms, S. Dasgupta, C. H. Papadimitriou, and U. V. Vazirani Python Algorithms-Mastering Basic Algorithms in

软件事务内存导论(二)软件事务内存

1.1    软件事务内存 将实体与状态分离的做法有助于STM(软件事务内存)解决与同步相关的两大主要问题:跨越内存栅栏和避免竞争条件.让我们先来看一下在Clojure上下文中的STM是什么样子,然后再在Java里面使用它. 通过将对内存的访问封装在事务(transactions)中,Clojure消除了内存同步过程中我们易犯的那些错误(见 <Programming Clojure>[Hal09]和<The Joy of Clojure>[FH11]).Clojure会敏锐地观察和

Pratical Cljr – loop/recur

Programming Clojure这块写的过于简单, Pratical Clojure写的还不错, 这本书在某些章节写的深度不错, 讨论why, 而不是仅仅how, 故摘录 首先, Clojure是不提供直接的looping语法的!!! 这个理解很重要 因为looping是imperative的概念, 对于FP, 需要改变思维的方式, 用递归来解决同样的问题, 所以下面也说对于从imperative 转来的程序员, 习惯从递归(recursive)的角度思考问题, 是很大的挑战. It wi

2010年美国计算机图书市场报告四:编程语言

导读:原文作者Mike Hendrickson发表的一篇<2010 State of the Computer Book Market, Post 4 - The Languages>,文中对各种编程语言的盘点进行分析.现将李松峰翻译文章<2010年美国计算机图书市场报告四:编程语言>转载.内容如下: 这是<2010年计算机图书市场报告>的第四部分,我们来看一看编程语言市场,对各种编程语言作一备盘点. 与2009年相比,2010年编程语言市场总体下滑,幅度为-6.27%