Multimethods, establishing priority of which gets called

I get this error:

Unable to resolve symbol: _ in this context

Apparently I can use _ as an argument to a function but not as a dispatch value. Is there any dispatch value I can use that means “I don’t care what value is in this spot”?

Assume I have this, which plays a big role in determining how I handle a JSON blob that has just arrived at my app:

 (defmulti make-choice (fn [params]
                        [
                         (get params :choice)
                         (true? (get params :item-id))
                         (allowed-item-type? (get params :item-type))
                         (true? (get params :page))
                         (number? (get params :page))
                         ]
                        ))

The second and third parameters are mutually exclusive, so I want something like this:

     (defmethod make-choice [_ true true _] [params]
      {:method (str "Error: your request had both item-id and item-type but they are mutually exclusive. " params)})

My question: is there a way I can get this to take priority over all other methods of this multi? That is, no matter what other combinations exist, if these two parameters are true, then this make-choice is triggered? Does it happen automatically or do I have to worry about something else happening?

I’d like to put all of my possible error states together like this:

(defmethod make-choice :default [params]
  {:message (str "We were unable to find that page or choice:" params)})

(defmethod make-choice [_ true true _ _] [params]
  {:method (str "Error: your request had both item-id and item-type but they are mutually exclusive. " params)})

(defmethod make-choice [_ true _ true _] [params]
  {:method (str "Error: your request had both item-id and a page parameter but they are mutually exclusive. " params)})

(defmethod make-choice [_ _ _ true true] [params]
  {:method (str "Error: your request had a page parameter but it was not an integer. " params)})

But can I be sure that these error states will always take precedence over any other combination of parameters that get sent to make-choice?

What you describe is called pattern matching. And multimethods don’t support it, they require specific values. The only generalization that can take place there is inheritance (if you register a multimethod for, say, Object, it will be called for Stringand other classes as well, unless a more specialized multimethod exists) and parent/child relationships (Clojure-specific, can be created withderives`).

You can use clojure.core.match/match for pattern matching, but it has its own downside - you cannot register new patterns from elsewhere, it all has to be in one place. But perhaps it’s a reasonable trade-off in your case.

1 Like

Unless you want “someone else” like a library consumer to define their own defmethods, consider a simple cond or pattern-matching dispatch table.

Multimethods shine in cases when open extension is needed.

2 Likes

I think maybe you can do what you want, but have it return true and false?

(defmulti make-choice (fn[params]
                        [(get params :choice)
                         (boolean (get params :item-id))
                         (boolean (get params :item-type))
                         (boolean (get params :page))
                         (number? (get params :page))]))

(defmethod make-choice :default [params]
  {:message (str "We were unable to find that page or choice:" params)})

(defmethod make-choice [:bar true true false false] [params]
  {:method (str "Error: your request had both item-id and item-type but they are mutually exclusive. " params)})

(defmethod make-choice [:bar true false true false] [params]
  {:method (str "Error: your request had both item-id and a page parameter but they are mutually exclusive. " params)})

(defmethod make-choice [:baz false false true true] [params]
  {:method (str "Error: your request had a page parameter but it was not an integer. " params)})

(make-choice {:choice :bar
              :item-id 123
              :item-type :soap})
;;> {:method "Error: your request had both item-id and item-type but they are mutually exclusive. {:choice :bar, :item-id 123, :item-type :soap}"}

There is no wildcard like * or _ though. You have to be explicit about all options.

2 Likes

Thank you for this.