I use multimethods for something like a human friendly API.
I have the example below. You don’t have to read it. I explain it.
The example has a multimethod answer-what-is-the, that is able to find the answers for various questions. Questions can be asked with a macro what-is-the, for example (what-is-the date of now). The macro converts date and of into keywords, so that the multimethod can dispatch on them.
This works, and I am very happy how it answers me so many questions. But I have the impression that the implementation is not ideal. There are two things, where I am sceptical.
Assume I want to write a Unit Test for the macro. I would have to isolate it. But is it possible to temporarily forget all the methods, define a few methods as stubs, do a test and then restore the methods that were there before the test.
I have the vague feeling, that this mechanism does not scale well. I cannot clearly say what bothers me. The list of methods and the whole dispatching is something global, which the developer has no control over. Things are compiled and then they are just there.
Is there a way how I can have more control over multimethods? Something like a global variable? I suppose, internally a multimethod should be something like a global information somewhere. But I don’t want to manipulate complicated Clojure internals. Does anybody see a better implementation than the following code?
(defmulti answer-what-is-the (fn [property of object] property))
(defmacro what-is-the [property of object]
(assert (= of 'of))
(list 'answer-what-is-the (keyword property) (keyword of) object))
(defmethod answer-what-is-the :value [property of object]
(:value object))
(defmethod answer-what-is-the :circumference [property of object]
(* 2 (+ (object :width) (object :height))))
(defn now [] (new java.util.Date))
(defmethod answer-what-is-the :date [property of object]
(.format (java.text.SimpleDateFormat. "MM/dd/yyyy") (object)))
"And here we go with a few examples."
(what-is-the value of {:value 18})
(what-is-the circumference of {:width 3 :height 5})
(what-is-the date of now)
There have been many, many times when the same lesson is learned by people writing APIs in Clojure. The lesson is: apart from very few exceptions, you should not write your API using macros. Even if they seem convenient.
Focus on plain functions instead, don’t try to make it readable as if it were English, require only the necessary information from a user, use keywords to signify names, use appropriate argument positions (the “main” object should go first to be fully compatible with the -> macro, the “main” collection should go last to be compatible with ->>).
In this particular case, I wouldn’t add that what-is-the macro. Instead, I’d have a single multimethod:
(defmulti get-property (fn [_object property] property))
(defmethod get-propery :value [object p]
(p obj))
[... and so on ...]
(get-property {:value 18} :value) ;; => 18
And if the set of properties is not open for extension, just have a bunch of predefined functions, like get-value, get-circumference, get-date, etc.
This automatically gets rid of your first question - there’s no need to test macros since there are no macros.
Regarding your second question, I’m not sure I understand it. Are you worried about property name clashes? If yes, then you can enforce usage of fully qualified keywords. Or don’t enforce anything and let your users decide for themselves.
Otherwise, I’m not sure what you mean. You can get more control over multimethods by providing a more sophisticated dispatch function or using hierarchies where appropriate, or maybe even writing your own defmulti-like thing that has its own rules. But I can’t tell what exactly would be best because you haven’t described what you need exactly. If you don’t have anything but that vague feeling and you can’t make it more concrete, I’d suggest going with plain multimethods till you encounter a tangible problem for which they are a bad fit, and then you’ll be much better equipped to decide what the next step should be.
Multimethods are typically for “function that I want other namespaces to be able to implement alternatives (or overrides) for”. My go-to example is in jdbc.next, where you can provide additional implementations of parsing PGObjects (postgres objects).
In your example, the more idiomatic approach would be to just use functions:
Re: human-friendly APIs, I guess it depends on who your target user is. If it’s another Clojure developer, then, “human-friendly” looks more like my example above (or for more complex systems, some sort of data-driven API). I talk a bit about different API design choices in this talk. “Reads like English” does not mean “easy to use API”.
If the intent is a DSL for a non-Clojure person, then, maybe… sure… your solution is fine. Or, you could go even more crazy with macros, or just write your own parser (say, with Instaparse).