4

Suppose we have a multimethod foo. It has several realizations. Let's say that one of them is called when argument of foo is a string that contains character \r and another is executed when argument of foo is a string containing character \!. Pseudocode:

(defmulti foo ???) ; can't come up with function..

(defmethod foo \r [_]
  (println "one"))

(defmethod foo \! [_]
  (println "two"))

So when we call our function like this:

(foo "right!") ;; desired output:
one
two
;; => nil

Important thing here is that list of supported methods should be not rigid, but expandable, so new methods can be added later without touching the original code.

Although I improved my Clojure skill significantly in last few days, I still lack experience. My best idea is to keep a map with pairs 'character - function' and then manually traverse it and execute right functions. In this case I will also need some interface to register new functions, etc. What is idiomatic solution?

Mark Karpov
  • 7,499
  • 2
  • 27
  • 62

2 Answers2

4

I think multimethods don't work the way you expect them to work.

That is: the dispatch in multimethods is called only once for a single multimethod call, so there's no way of getting the result you expect (both 'one' and 'two' printed for "right!" as argument) unless you define one implementation that actually handles the case of having both \r and \! in the input string and prints the output you want.

This will not be easily expandable.

Nicer way to achieve what you want is to make multiple calls explicitly by iterating the input string:

; You want the dispatch function to just return the character passed to it.
(defmulti foo identity) 

; The argument list here is mandatory, but we don't use them at all, hence '_'
(defmethod foo \r [_] 
  (println "one"))

(defmethod foo \! [_]
  (println "two"))


; You need the default case for all the other characters
(defmethod foo :default [_]
  ())

; Iterates the string and executes foo for each character
(defn bar [s] 
    (doseq [x s] 
        (foo x)))

so calling

(bar "right!") 

will print:

one
two

Edit

If you need to access the whole string inside the multimethod body, then pass it explicitly together with the character:

; You want the dispatch function to just return the character passed to it as the first arg.
(defmulti foo (fn [c _] c)) 


(defmethod foo \r [c s] 
  (println "one"))

(defmethod foo \! [c s]
  (println "two"))

; The default now takes two arguments which we ignore
(defmethod foo :default [_ _] ())

; Iterates the string and executes foo for each character
(defn bar [s] 
    (doseq [x s] 
        (foo x s)))
soulcheck
  • 36,297
  • 6
  • 91
  • 90
  • i) I know that it's not possible to accomplish with multimethods (I've read a book! ;-) but multimethod is the closest abstraction that came to my mind. ii) I fear I cannot use your solution, since methods for both `\r` and `\!` must have access to whole string. – Mark Karpov Aug 28 '14 at 12:55
  • @Mark so you need access to the whole string inside the multimethod? – soulcheck Aug 28 '14 at 13:00
  • Yes, that's the case. Think of `\r` and `\!` as of some qualities of argument. I wish to have several methods and every one of them is specialized on processing of whole object only if it has certain quality. So strings that contain `\r` will trigger one method, strings that contain `\!` will cause another one, and strings that contain both will trigger both methods, and there may be more of them. – Mark Karpov Aug 28 '14 at 13:06
0

A plain list of functions would allow arbitrary conditionals. Also Regexs may make your life simpler if you are dealing with strings:

;; start with some functions
(defn on-r [x]
  (when (re-find #"r" x)
    "one"))
(defn on-! [x]
  (when (re-find #"!" x)
    "two"))
(def fns (atom [on-r on-!]))

;; call all functions on some value
(keep #(% "right!") @fns)
=> ("one" "two")
(keep #(% "aaaa") @fns)
=> ()

;; later add more functions
(defn on-three [x]
  (when (= 3 (count x))
    "three"))
(swap! fns conj on-three)
(keep #(% "bar") @fns)
=> ("one" "three")

;; or just use different rules altogether
(def other-fns [#(when (rand-nth [true false])
                   (str % (rand-int 10)))
                #(when (nil? %) "nil")])
(keep #(% nil) other-fns)
=> ("3" "nil")
Timothy Pratley
  • 10,586
  • 3
  • 34
  • 63
  • I'm sorry, but this is just an example. I was interested in combining methods in Clojure in general rather than solving this specific problem. Moreover, as I said "Important thing here is that list of supported methods should be not rigid, but expandable, so new methods can be added later without touching the original code." – Mark Karpov Sep 01 '14 at 04:43
  • Hi Mark, thanks for the clarification. To explain my thinking, a list of functions is not bound to using a pre-defined set of conditionals. You can modify the list, or use different lists, or combinations of lists. I've amended my answer to demonstrate this. My point is that to check a bunch of stuff about some value, multi-methods are a useful but orthogonal concept, and the interesting combinatorial technique is function composition. Just my opinion of course, and I understand you are more interested in exploring strictly multi-methods. – Timothy Pratley Sep 01 '14 at 06:56