defmulti

added
1.0

ns
clojure.core

type
macro

(defmulti name docstring? attr-map? dispatch-fn & options)

Creates a new multimethod with the associated dispatch function.
The docstring and attr-map are optional.

Options are key-value pairs and may be one of:

:default

The default dispatch value, defaults to :default

:hierarchy

The value used for hierarchical dispatch (e.g. ::square is-a ::shape)

Hierarchies are type-like relationships that do not depend upon type
inheritance. By default Clojure's multimethods dispatch off of a
global hierarchy map.  However, a hierarchy relationship can be
created with the derive function used to augment the root ancestor
created with make-hierarchy.

Multimethods expect the value of the hierarchy option to be supplied as
a reference type e.g. a var (i.e. via the Var-quote dispatch macro #'
or the var special form).

                ;; Define the multimethod
(defmulti service-charge (fn [acct] [(account-level acct) (:tag acct)]))

;; Handlers for resulting dispatch values
(defmethod service-charge [::acc/Basic   ::acc/Checking] [_] 25)
(defmethod service-charge [::acc/Basic   ::acc/Savings]  [_] 10)
(defmethod service-charge [::acc/Premium ::acc/Account]  [_] 0)
            
                ;this example illustrates that the dispatch type
;does not have to be a symbol, but can be anything (in this case, it's a string)

(defmulti greeting
  (fn[x] (x "language")))

;params is not used, so we could have used [_]
(defmethod greeting "English" [params]
 "Hello!")

(defmethod greeting "French" [params]
 "Bonjour!")

;;default handling
(defmethod greeting :default [params]
 (throw (IllegalArgumentException. 
          (str "I don't know the " (params "language") " language"))))

;then can use this like this:
(def english-map {"id" "1", "language" "English"})
(def  french-map {"id" "2", "language" "French"})
(def spanish-map {"id" "3", "language" "Spanish"})

=>(greeting english-map)
"Hello!"
=>(greeting french-map)
"Bounjour!"
=>(greeting spanish-map)
 java.lang.IllegalArgumentException: I don't know the Spanish language
            
                ;; Implementing factorial using multimethods Note that factorial-like function 
;; is best implemented using `recur` which enables tail-call optimization to avoid 
;; a stack overflow error. This is a only a demonstration of clojure's multimethod

;; identity form returns the same value passed
(defmulti factorial identity)

(defmethod factorial 0 [_]  1)
(defmethod factorial :default [num] 
    (* num (factorial (dec num))))

(factorial 0) ; => 1
(factorial 1) ; => 1
(factorial 3) ; => 6
(factorial 7) ; => 5040
            
                ;; defmulti/defmethods support variadic arguments and dispatch functions.

(defmulti bat 
  (fn ([x y & xs] 
       (mapv class (into [x y] xs)))))
(defmethod bat [String String] [x y & xs] 
  (str "str: " x " and " y))
(defmethod bat [String String String] [x y & xs] 
  (str "str: " x ", " y " and " (first xs)))
(defmethod bat [String String String String] [x y & xs] 
  (str "str: " x ", " y ", " (first xs) " and " (second xs)))
(defmethod bat [Number Number] [x y & xs] 
  (str "number: " x " and " y))

;; you call it like this...

(bat "mink" "stoat")
;; => "str: mink and stoat"

(bat "bear" "skunk" "sloth")
;; => "str: bear, skunk and sloth"

(bat "dog" "cat" "cow" "horse")
;; => "str: dog, cat, cow and horse"

(bat 1 2)
;; => "number: 1 and 2"
            
                ;; defmulti - custom hierarchy

(def h (-> (make-hierarchy)
           (derive :foo :bar)))

(defmulti f identity :hierarchy #'h) ;; hierarchy must be a reference type

(defmethod f :default [_] "default")
(defmethod f :bar [_] "bar")

(f :unknown) ;; "default"
(f :bar) ;; "bar"
(f :foo) ;; "bar"

;; Note that any deref'able type is fine. 
;; Using an atom instead of (var h) is preferable in clojurescript 
;; (which adds a lot of meta information to vars)
            
                ;; If you're REPLing you might want to re-define the defmulti dispatch function 
;; (which defmulti won't allow you to do). For this you can use `ns-unmap`:

(defmulti x (fn[_] :inc))
(defmethod x :inc [y] (inc y))
(defmethod x :dec [y] (dec y))
(x 0) ;; => 1
(defmulti x (fn[_] :dec)) ;; Can't redefine :(
(x 0) ;; => 1 ;; STILL :(
(ns-unmap *ns* 'x) ;; => unmap the var from the namespace
(defmulti x (fn[_] :dec))
(x 0) ;; => Exception, we now need to redefine our defmethods.

;; So in your file while developing you'd put the ns-unmap to the top of the file

            
                ;; It's nice for multimethods to have arglists metadata so that calling `doc`
;; prints the arglist, instead of just the docstring. For example:

(defmulti f "Great function" (fn [x] :blah))
(doc f)
;; -------------------------
;; user/f
;;  Great function

;; However, we can add `:arglists` metadata via a third (optional) argument to `defmulti` (`attr-map?` in the docstring for `defmulti`):

(defmulti g "Better function" {:arglists '([x])} (fn [x] :blah))
(doc g)
;; -------------------------
;; user/f
;; ([x])
;;  Better function

            
                (defmulti compact map?)

(defmethod compact true [map]
  (into {} (remove (comp nil? second) map)))

(defmethod compact false [col]
  (remove nil? col))

; Usage:

(compact [:foo 1 nil :bar])
; => (:foo 1 :bar)

(compact {:foo 1 :bar nil :baz "hello"})
; => {:foo 1, :baz "hello"}

            
                ;; This show how to do a wildcard match to a dispatch value:
(defmulti xyz (fn [x y] [x y]))

;; We don't care about the first argument:
(defmethod xyz [::default :b]
  [x y]
  :d-b)

;; We have to implement this manually:
(defmethod xyz :default
  [x y]
  (let [recover (get-method xyz [::default y])]
    ;; Prevent infinite loop:
    (if (and recover (not (= (get-method xyz :default) recover)))
      (do
        (println "Found a default")
        ;; Add the default to the internal cache:
        ;; Clojurescript will want (-add-method ...)
        (.addMethod ^MultiFn xyz [x y] recover)
        (recover ::default y))
      :default)))

(xyz nil :b) ;; => :d-b
;; only prints "Found a default" once!
            
                ;; Extremely simple example, dispatching on a single field of the input map.
;; Here we have a polymorphic map that looks like one of these two examples:

;;  {:name/type :split :name/first "Bob" :name/last "Dobbs"}
;;  {:name/type :full :name/full "Bob Dobbs"}

(defmulti full-name :name/type)

(defmethod full-name :full [name-data] 
  (:name/full name-data))

(defmethod full-name :split [name-data] 
  (str (:name/first name-data) " " (:name/last name-data)))

(defmethod full-name :default [_] "???")

(full-name {:name/type :full :name/full "Bob Dobbs"})
;; => "Bob Dobbs"

(full-name {:name/type :split :name/first "Bob" :name/last "Dobbs"})
;; => "Bob Dobbs"

(full-name {:name/type :oops :name/full "Bob Dobbs"})
;; => "???"

            
                ;;polymorphism classic example

;;defmulti
(defmulti draw :Shape)

;;defmethod
(defmethod draw :Square [color] (str "Drawing a " (:clr color) " square"))
(defmethod draw :Triangle [color] (str "Drawing a " (:clr color) " triangle"))

(defn square [color] {:Shape :Square :clr color})
(defn triangle [color] {:Shape :Triangle :clr color})

(draw (square "red"))
(draw (triangle "green"))
            
                ;;defmulti with dispatch function
(defmulti salary (fn[amount] (amount :t)))

;;defmethod provides a function implementation for a particular value
(defmethod salary "com" [amount] (+ (:b amount) (/ (:b amount) 2)))
(defmethod salary "bon" [amount] (+ (:b amount) 99))

(salary {:t "com" :b 1000}) ;;1500
(salary {:t "bon" :b 1000}) ;;1099