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 cat
s. 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.
精彩评论