Tuesday, September 28, 2010

Dynamic mixins in Clojure: an experiment.

My web-friend Debasish Ghosh recently come up with an excellent post about modeling the same domain with Scala and Clojure, focusing in particular on how to implement dynamic behavior through Scala mixins and Clojure combinators.

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!

12 comments:

Javier said...

great "experiment", i think is totally usable and idiomatic. I tried to mimic the scala way of related post (this code) but one diff was the static bind between protocol and record as you have pointed, althoug adding another level of indirection associate outer functions in protocol impl.
Have you tried tou use reify? it could to be a half way to add behaviours less dynamic than map but more than with extend-type.

Sergio Bossa said...

Hi Javier,

thanks for your interest and kind words!

Regarding "reify", yes, I thought about it and you could actually use it in my example to directly implement the protocols, rather than explicitly defining a type (as I did mostly for clarity).
But I don't see how could "reify" replace the map association: may you elaborate on that?

Javier said...

In the gist linked in my prev comment i've added a reify aproximation, but it doesnt replace the map with mixins associated with keys and types as traits, it is a different way for learning and experimenting and i dont know if the workaround has sense.
With your example the code'd be:

(with-outputter [j] (reify Outputter (output [_ logic] (logic j)))
(output (with-outtputter joe) #(println (:name %1)))

The bind is make with fn call with a closure and dont change joe out of output call (this can be useful or not), and i think maybe it could be applied for adapting old data and fns to new protocols.

Sergio Bossa said...

Your code makes certainly sense, in particular if you have to bind an existent record to a limited set of protocols.
But, it's still static: the with-x function strictly refers to a particular protocol, and you can't mix several protocols this way.

Or did I miss something?

Thanks for sharing your thoughts!

Javier said...

Certainly it's not dynamic, maybe more closed to scala. You have to redefine the function with-x (or the function called in it) if you wants to change the behaviour. I've only added more indirections between the data and the protocol.
I think being clojure dynamic per se your solution fits well, but maybe the mixing could live in metadata of Joe (where clojure puts the info about types) and no with its values.

Sergio Bossa said...

The idea of putting the "traits map" into the object metadata sounds very good indeed: I've never found a use case to play with clojure metadata map, so I'll give it a try.

Thanks for sharing your thoughts and the nice discussion!

nickikt said...

nice post.

would be cool if i could track your blog on http://planet.clojure.in/.

Sergio Bossa said...

Thanks for your kind words nickickt!
Obviously, feel free to aggregate my blog on http://planet.clojure.in/ ... cheers!

Alex Ott said...

Sergio - I added your blog to Clojure planet several days ago

Vivian Salvatore said...

The best Guild Wars 2 Gold about knowledge is that it is frequently easier to acquire expertise as compared to obtain time for it to exercise. What do you should know?1st, educate yourself on the Buy D3 Gold. Discover YOUR school. Ensure you determine what the abilities are (regardless of whether anyone "plan" to utilize them or otherwise not).

三文鱼 said...

Now, while the publish is outstanding as always, and Clojure combinators are fairly awesome, I think a sector design is better showed with Cheap WOW Gold from http://www.fitwowgold.com/ Clojure information and methods, so let me explain to you a little research about how to apply powerful mixins in Clojure.

hugginss jetterees said...

Smartphone makers have faced a equivalent office 2010 professional plus key predicament ahead of. Google in mid-2011 launched a $12.5 billion bid for Motorola, raising worries that the merger would threaten the independence of Android. So far, that hasn't occurred. Still, some vendors, including Samsung, are exploring alternatives windows 8 pro retail box just like the Tizen open supply operating method.