Organizing Clojure code - A real problem?

I do think #1, #2, #3 and #4 are all a part of it.

On Slack, I recently showed someone. They had this example:

 (ns fruit.protocols)
 
 (defprotocol FruitConvert
   (make-juice [fruit])
   (make-tart [fruit]))
 (ns fruit.juice)
 
 (defrecord Juice [color])
 (ns fruit.apple
   (:require [fruit.protocols :as p]
             [fruit.juice :as j]))
 
 (defrecord Apple [name]
   p/FruitConvert
   (make-juice [_]
     (j/->Juice "red")))

And didn’t know how to structure things. So this was my advice:

Hum… What I normally do is define my entire domain in a single namespace I call domain.clj. What I put in there is only the data schemas, so it would be data specs or records. Then the rest of my application would be defining operations over this domain data. If I want conversions between my entities for example, I could define a conversion.clj namespace.

Now honestly, I wouldn’t use protocols for this, I’d just have normal functions called X->Y, X->Z, Y->Z, etc. Its much clearer to me that way.

Do you have a reason to want a protocol? Like do you need to have a generic conversion where you don’t know what you are getting out of multiple possible type and need it converted to a Z ?

That would be the question I think that can answer your question. If you want “one of many possible types”->Z then I’d just create a protocol with a ->Z definition or a make-z-from kind of thing. I’d put this protocol in the conversion.clj namespace and I’d extend all the types to implement it in that namespace as well, but the records would all be in the same domain.clj namespace.

So what happens is the namespaces just group conceptually similar kind of functionality. They don’t group all functionality related to a particular record. The latter would be what OO does, OO will say, ok group all functions that operates on a given type together. This just seems wrong to me in Clojure. So I don’t put convertions from/to Z in the Z namespace. Instead I put all conversions from any entity to any other in a conversion namespace.

And here was my restructure of their example:

 (ns domain)
 
 (defrecord Juice [color])
 (defrecord Apple [name])
 (ns conversions
   (:require [domain :as d]))
 
 (defprotocol Juiceable
   (make-juice [juiceable]))
 
 (defprotocol Tarteable
   (make-tart [tarteable]))
 
 (extend-protocol Juiceable
   domain.Apple
   (make-juice [_apple] (d/->Juice "red")))
 (ns app
   (:require [domain :as d]
             [conversions :as c]))
 
 (c/make-juice (d/->Apple "Hello"))
 ;;=> #domain.Juice{:color "red"}

Their answer was:

Thanks! I think I’ve seen the “Namespaces group functionality, not abstractions” advice elsewhere, but this made it finally click :slightly_smiling_face:

So hopefully this helps others as well. And my thought is, probably we shouldn’t have an answer in words only, we need to show example of how to structure things, maybe even better how to go from what they’d do to what they should have done.

14 Likes