Now, while the post is excellent as always, and Clojure combinators are pretty cool, I think a domain model is better represented with Clojure records and protocols, so let me show you a little experiment about how to implement dynamic mixins in Clojure.
This is my first Clojure-related blog post, and I'm no way a Clojure expert, so take it with care and feel free to hand me any kind of feedback ;)
First, for those unfamiliar with records and protocols, there are a few good articles around: to sum up in a single sentence, protocols are a way to describe a contract made up of functions you have to implement in order to adhere to the protocol itself, while records (and types) define data and implement protocols.
So, let's define a Person record and a simple Outputter protocol for outputting things:
(defrecord Person [name])
(defprotocol Outputter
(output [self logic])
)
The record defines only the person name, and the protocol defines an output function whose actual execution logic is plugged from the outside through the logic argument.
The most common way to implement a protocol is to extend a record type and implement the proper behavior:
(extend-type Person
Outputter
(output [self logic] (println (:name self)))
)
Glancing through the code, we're extending the Person record and implementing the Outputter by printing out the person name.
But there's a problem with that: the behavior is static. More specifically:
- You can't dynamically remove protocol implementations.
- Added protocol implementations are shared by all record instances, so you can't compose protocol implementations with specific record instances.
Let's try to come up with a solution.
First, let's define the function that is at the core of our experiment: the mix function.
(defn mix [source mixins]
(let [m (merge source mixins)] #(get m %1))
)
Very short and concise, isn't it? We'll come back to it in a moment.
Then, let's define a simple record instance and two protocol implementations (without extending any record this time): we'll call them, guess what, traits.
(def joe (Person. "joe"))
(deftype OutputterTrait [target]
Outputter
(output [self logic] (logic target))
)
(deftype DummyTrait []
Outputter
(output [self logic] (println "ignored external logic"))
)
We defined a simple record with no protocols implemented, and two separated protocol implementations.
Now, let's mix them up!
(def mixed (mix joe {:outputter (OutputterTrait. joe) :dummy (DummyTrait)}))
What's up?
We just used our previous mix function to dynamically associate joe with two different protocol instances, each one identified by a particular keyword.
Now, we can output our mixed Joe calling both protocol implementations:
(output (mixed :outputter) #(println (:name %1)))
(output (mixed :dummy) #(println "this will be ignored"))
The trick is the mix function that dynamically associates the record instance with protocol implementations identified by keyword, later used to recall the protocol back.
We've got dynamic mixins so, but at the cost of depending on an "external" keyword we have to be careful about: passing the wrong keyword may in fact lead to the wrong protocol implementation.
So, always prefer the classical, built-in way to use protocols and records ... but feel free to experiment with dynamic mixins when needing dynamic behavior!
Any feedback will be highly appreciated ... enjoy and see you next time!