开发者

Clojure defprotocol as a solution to the expression problem

In the book "The Joy of Clojure", defprotocol is offered as a solution to the expression problem -- "the desire to implement an existing set of abstract methods for an existing concrete class without having to change the code that defines either."

The example given is as follows:

(defprotocol Concatenatable
  (cat [this other]))

(extend-type String
  Concatenatable
  (cat [thi开发者_如何转开发s other]
    (.concat this other)))

(cat "House" " of Leaves")
;=> "House of Leaves"

(extend-type java.util.List
  Concatenatable
  (cat [this other]
    (concat this other)))

(cat [1 2 3] [4 5 6])
;=> (1 2 3 4 5 6)

It is suggested that this is not possible in a language like Java, but how is it different than the following?

public class Util {
  public static String cat(final String first,
                           final String second) {
    return first.concat(second);
  }

  public static <T> List<T> cat(final List<T> first,
                                final List<T> second) {
    final List<T> list = new List<T>(first);
    list.addAll(second);
    return list;
  }
}

After all, both are used similarly:

(cat "House" " of Leaves")
Util.cat("House", " of Leaves");

The Clojure function cat is not a method on the String and List classes, but rather an independent function that is overloaded to accept either String or List arguments.

Although I really like Clojure, I don't understand the claims of superiority for this construct.


Okay. You release this cat Java library to much fanfare, and everyone downloads it. It's so great I want to make my own TVCommercial type be concatenable so that I can send it to bits of your library that operate on concatenable objects.

But I can't, because you call Util.cat(obj1, obj2), which has no overload for TVCommercial. I can't extend your code to handle my types, because I don't own your code.

You can define Concatenable as an interface to address this problem:

interface Concatenable {
  Concatenable cat(Concatenable other);
}

But now I can't write a class which is both Concatenable and... I don't know, an AnimalHandler, that handles cats. Clojure's protocols solve both problems by decentralizing the dispatch functions and implementations: they live all over the place, rather than in some single location. In Java, you choose between:

  • Putting all your type dispatch into a single switch/case or overloaded method
  • Defining an interface mandating a method with a particular name

Clojure basically does the latter of these, but because it uses namespaced names, there's no danger of conflict with other protocols that think cat is a good function name.


Each time a new type comes along to which you'd like to apply your cat function, you need to "reopen" your Util class and add method overloads for the new target types.

The Expression Problem seeks to avoid this need, such that existing types aren't disturbed by new operations being defined and existing implemented operations aren't disturbed by new types that want to participate in those operations. The Clojure protocol example shown here doesn't fulfill the first goal, as adding a new function to a published protocol requires that all types to which the protocol is already extended define an implementation for that new method.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜