zoukankan      html  css  js  c++  java
  • 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的时候...

  • 相关阅读:
    第六周学习总结
    20165227 《Java程序设计》实验一(Java开发环境的熟悉)实验报告
    第五周学习总结
    20165227 20165228结对学习感想
    第四周课堂测试补做
    第四周作业
    20165227第三周学习总结
    第二周学习总结
    20165304《JAVA程序设计》第四周学习总结
    20165304 2017-2018-2 《Java程序设计》第3周学习总结
  • 原文地址:https://www.cnblogs.com/fxjwind/p/2936471.html
Copyright © 2011-2022 走看看