<jupyter_start><jupyter_text>Multimethod polymorphism  Polymorphism and its types  Ad-hoc polymorphism "closed-dispatch" polymorphism<jupyter_code>; A function that ckecks the type of 'thing' argument 
; to call different functions based on the type

(defn ad-hoc-type-namer [thing]
    (condp = (type thing) 
        java.lang.String "string" 
        clojure.lang.PersistentVector "vector"))

; Passing a string parameter

(ad-hoc-type-namer "I'm a string")
; Passing a vector parameter

(ad-hoc-type-namer [])
; If there’s a type this function doesn’t know how to handle, an exception is thrown

(ad-hoc-type-namer {})<jupyter_output>Execution error (IllegalArgumentException) at user/ad-hoc-type-namer (REPL:5).
No matching clause: class clojure.lang.PersistentArrayMap<jupyter_text>"open-dispatch" polymorphism<jupyter_code>; Pull type implementations out into a separate map

(def type-namer-implementations 
    {java.lang.String (fn [thing] "string")
     clojure.lang.PersistentVector (fn [thing] "vector")})

; A function that ckecks the type of 'thing' argument 
; to call different functions based on the type, 
; without a fixed set of types to ckeck
 
(defn open-ad-hoc-type-namer [thing]
    (let [dispatch-value (type thing)] 
        ; Use a dispatch value as key to implementation map
        (if-let [implementation (get type-namer-implementations dispatch-value)] 
            (implementation thing) 
            (throw (IllegalArgumentException. (str "No implementation found for " dispatch-value))))))

; Passing a parameter of string type

(open-ad-hoc-type-namer "I'm a string") 
; Passing a parameter of vector type

(open-ad-hoc-type-namer []) 
; Passing a parameter of map type (it will throw an exception)

(open-ad-hoc-type-namer {}) 
; Adding a 'map' type to a the map of types

(def type-namer-implementations 
    (assoc type-namer-implementations clojure.lang.PersistentArrayMap (fn [thing] "map")))

; Passing a parameter of map type again (it won't throw an exception now)

(open-ad-hoc-type-namer {})<jupyter_output><empty_output><jupyter_text>Subtype polymorphism Checking types with ad-hoc polymorphism<jupyter_code>; A function that checks if its argument is map-like

(defn map-type-namer [thing] 
    (condp = (type thing)
        clojure.lang.PersistentArrayMap "map"
        clojure.lang.PersistentHashMap "map")) ; Notice the code duplication
; Passing a parameter of hash-map type (recognized)

(map-type-namer (hash-map))
; Passing a parameter of array-map type (recognized)

(map-type-namer (array-map))
; Passing a parameter of sorted-map type (unrecognized)

(map-type-namer (sorted-map))<jupyter_output>Execution error (IllegalArgumentException) at user/map-type-namer (REPL:4).
No matching clause: class clojure.lang.PersistentTreeMap<jupyter_text>Checking types with subtype polymorphism<jupyter_code>(defn subtyping-map-type-namer [thing] 
    (cond
        (instance? clojure.lang.APersistentMap thing) "map" ; APersistent-Map is Java superclass of all map-like things in Clojure
         :else (throw (IllegalArgumentException. (str "No implementation found for ") (type thing)))))

; Passing a parameter of hash-map type (recognized)

(subtyping-map-type-namer (hash-map))
; Passing a parameter of array-map type (recognized)

(subtyping-map-type-namer (array-map))
; Passing a parameter of sorted-map type (recognized now)

(subtyping-map-type-namer (sorted-map))<jupyter_output><empty_output><jupyter_text>Polymorphism using multimethods Implementing a tracking expense service without multimethods<jupyter_code>; A mapping for a sample user

(def example-user {:login "rob" 
                   :referrer "mint.com" 
                   :salary 100000})

; A function that calculates the fee depending of the referrer type

(defn fee-amount [percentage user]
    (with-precision 16 :rounding HALF_EVEN 
        ; Using BigDecimal when dealing with money
        (* 0.01M percentage (:salary user))))
; A function that performs close dispatch based on the referrer type

(defn affiliate-fee [user]
    (case (:referrer user)
        "google.com" (fee-amount 0.01M user)
        "mint.com" (fee-amount 0.03M user)
        (fee-amount 0.02M user)))

; Testing the last function with the example-user map

(affiliate-fee example-user)<jupyter_output><empty_output><jupyter_text>Implementing a tracking expense service with multimethods<jupyter_code>; A general form of the defmulti macro

"""
(defmulti name docstring? attr-map? dispatch-fn & options)
"""
; The defmulti returns a dispatch value 
; with which to find an implementation.

(defmulti affiliate-fee 
    (fn [user] (:referrer user)))

; Defining an implementation for the "mint.com" dispatch value

(defmethod affiliate-fee "mint.com" [user] 
    (fee-amount 0.03M user))
; Defining an implementation for the "google.com" dispatch value

(defmethod affiliate-fee "google.com" [user]
    (fee-amount 0.01M user))
; Defining an implementation for a default dispatch value

(defmethod affiliate-fee :default [user] 
    (fee-amount 0.02M user))
; Testing the first method with the example-user map

(affiliate-fee example-user)<jupyter_output><empty_output><jupyter_text>Using multimethods with custom defaults<jupyter_code>; Redefining the default case (it won't take effect)

(defmulti affiliate-fee :referrer :default "*")
; Removing the affiliate-fee multimethod from the user namespace

(ns-unmap 'user 'affiliate-fee)
; Redefining the default case (it will take effect now)

(defmulti affiliate-fee :referrer :default "*") 
; Defining the new default case method

(defmethod affiliate-fee "*" [user] 
    (fee-amount 0.02M user))
(affiliate-fee example-user)
; A general form of the defmethod macro

"""
(defmethod multifn dispatch-value & fn-tail)
"""

; The defmethod macro can also be used with multiple arities

"""
(defmethod my-many-arity-multi :default
    ([] \"no arguments\")
    ([x] \"one argument\")
    ([x & etc] \"many arguments\"))
"""<jupyter_output><empty_output><jupyter_text>Accessing multimethod map items<jupyter_code>; Defining again the method for "mint.com"

(defmethod affiliate-fee "mint.com" [user] 
    (fee-amount 0.03M user))
; Defining again the method for "google.com"

(defmethod affiliate-fee "google.com" [user]
    (fee-amount 0.01M user))
; Returning all the methods associated to "affiliate-fee"

(methods affiliate-fee) 
; Returning the method associated to "mint.com"

(get-method affiliate-fee "mint.com") 
; No specific method associated to "example.org"

(get (methods affiliate-fee) "example.org")
; The "*" (default) implementation

(get-method affiliate-fee "example.org") 
; The entries in the dispatch map can be called as normal functions.

((get-method affiliate-fee "mint.com") example-user)<jupyter_output><empty_output><jupyter_text>Multiple dispatch Creating more mappings of user categories<jupyter_code>(def user-1 {:login "rob" :referrer "mint.com" :salary 100000
             :rating :rating/bronze})
(def user-2 {:login "gordon" :referrer "mint.com" :salary 80000
             :rating :rating/silver})
(def user-3 {:login "kyle" :referrer "google.com" :salary 90000
             :rating :rating/gold})
(def user-4 {:login "celeste" :referrer "yahoo.com" :salary 70000
             :rating :rating/platinum})<jupyter_output><empty_output><jupyter_text>The new business rules are as follows:```Affiliate       Profit rating       Fee (% of salary)mint.com        Bronze              0.03mint.com        Silver              0.04mint.com        Gold/platinum       0.05google.com      Gold/platinum       0.03```<jupyter_code>; A keyword function that extracts the 2 parameters of interest (referrer and rating)

(defn fee-category [user]
    [(:referrer user) (:rating user)])

(map fee-category [user-1 user-2 user-3 user-4])
; Rewriting the fee-amount function

(defn fee-amount [percentage user]
    (with-precision 16 :rounding HALF_EVEN 
        ; Using BigDecimal when dealing with money
        (* 0.01M percentage (:salary user))))
; A multimethod that implements the above business rules table

(defmulti profit-based-affiliate-fee 
    fee-category) 

(defmethod profit-based-affiliate-fee ["mint.com" :rating/bronze] [user] 
    (fee-amount 0.03M user))

(defmethod profit-based-affiliate-fee ["mint.com" :rating/silver] [user] 
    (fee-amount 0.04M user))

(defmethod profit-based-affiliate-fee ["mint.com" :rating/gold] [user] 
    (fee-amount 0.05M user))

(defmethod profit-based-affiliate-fee ["mint.com" :rating/platinum] [user] 
    (fee-amount 0.05M user))

(defmethod profit-based-affiliate-fee ["google.com" :rating/gold] [user] 
    (fee-amount 0.03M user))

(defmethod profit-based-affiliate-fee ["google.com" :rating/platinum] [user] 
    (fee-amount 0.03M user))

(defmethod profit-based-affiliate-fee :default [user] 
    (fee-amount 0.02M user))
; Testing the multimethod with the 4 initial users

(map profit-based-affiliate-fee [user-1 user-2 user-3 user-4])<jupyter_output><empty_output><jupyter_text>Subtype polymorphism using multimethods Building type hierarchies with 'derive'<jupyter_code>; Using :rating/ANY as root type for all ratings

(derive :rating/basic :rating/ANY) 
(derive :rating/premier :rating/ANY)
; Deriving the basic 'class'

(derive :rating/bronze :rating/basic) 
(derive :rating/silver :rating/basic) 

; Deriving the premier 'class'

(derive :rating/gold :rating/premier)
(derive :rating/platinum :rating/premier)<jupyter_output><empty_output><jupyter_text>Inspecting the newly created hierarchy with the following keywords<jupyter_code>(isa? :rating/gold :rating/premier)
; isa? understands transitive relationships, too

(isa? :rating/gold :rating/ANY) 
(isa? :rating/ANY :rating/premier)
; Types are always kinds of themselves.

(isa? :rating/gold :rating/gold) 
; Ancestors returns parents and their parents

(parents :rating/premier) 
(ancestors :rating/gold) 
; Desendants returns children and their children

(descendants :rating/ANY) 
; Using the last type hierarchy to create a new multimethod

(defmulti greet-user :rating)

(defmethod greet-user :rating/basic [user]
    (str "Hello " (:login user) \.))

(defmethod greet-user :rating/premier [user]
    (str "Welcome, " (:login user) ", valued affiliate member!"))

; Testing the new multimethod with the 4 initial users

(map greet-user [user-1 user-2 user-3 user-4])<jupyter_output><empty_output><jupyter_text>Refactoring the profit-based-affiliate-fee function to use typw hierarchies and multimethods<jupyter_code>; Removing the methods that will be refactored

(remove-method profit-based-affiliate-fee ["mint.com" :rating/gold]) 
(remove-method profit-based-affiliate-fee ["mint.com" :rating/platinum])
(remove-method profit-based-affiliate-fee ["google.com" :rating/gold])
(remove-method profit-based-affiliate-fee ["google.com" :rating/platinum])

; Defining the new methods that use the custom type hierarchy

(defmethod profit-based-affiliate-fee ["mint.com" :rating/premier] [user] 
    (fee-amount 0.05M user))

(defmethod profit-based-affiliate-fee ["google.com" :rating/premier] [user] 
    (fee-amount 0.03M user))
; Testing the refactored multimethod

(map profit-based-affiliate-fee [user-1 user-2 user-3 user-4])<jupyter_output><empty_output><jupyter_text>Resolving method ambiguities<jupyter_code>; Defining a multimethod with potential conflicts

(defmulti size-up (fn [observer observed]
    [(:rating observer) (:rating observed)]))

(defmethod size-up [:rating/platinum :rating/ANY] [_ observed] 
    (str (:login observed) " seems scrawny."))

(defmethod size-up [:rating/ANY :rating/platinum] [_ observed] 
    (str (:login observed) " shimmers with an unearthly light."))
; Testing the above multimethod (it will throw an exception)

(size-up {:rating :rating/platinum} user-4) 
; Resolving the ambiguity with the 'prefer' keyword

(prefer-method size-up [:rating/ANY :rating/platinum] [:rating/platinum :rating/ANY]) 

; Testing the above multimethod (it will work now)

; The method with '[:rating/ANY :rating/platinum]' signature is preferred
(size-up {:rating :rating/platinum} user-4) 
; Listing all preferred implementations

(prefers size-up)<jupyter_output><empty_output><jupyter_text>User-defined hierarchies<jupyter_code>; Hierarchies are just ordinary maps.

(def myhier (make-hierarchy))
myhier 

; Adding a new element to a hierarchy don't mutate the original one

(derive myhier :a :letter) 

; The original hierarchy is the same

myhier
; To create ammutable hierarchy, it should be defined as a Var
 
(def myhier 
    (-> myhier 
        (derive :a :letter) 
        (derive :b :letter)
        (derive :c :letter)))

; The isa? method also works with custom hierarchies

(isa? myhier :a :letter) 
; The parents method also works with custom hierarchies

(parents myhier :a) 
; A multimethod that accepts a custom hierarchie as a parameter (as a Var)

(defmulti letter? identity :hierarchy #'myhier) 

(defmethod letter? :letter [_] true)

; Calling the multimethod with an item that doesn't exist in the hierarchy (It will fail)

(letter? :d)
; Adding the missing element and calling the multimethod again (It will work now)

(def myhier (derive myhier :d :letter)) 

(letter? :d)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11240280818298483, 'fraction_numerical': 0.009813542688910697, 'mean_word_length': 4.350565428109855, 'pattern_counts': {'":': 0, '<': 59, '<?xml version=': 0, '>': 60, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '607378', 'license': 'MIT', 'n_tokens_mistral': 4473, 'n_tokens_neox': 4228, 'n_words': 1318, 'path': 'chapter04/chapter04.ipynb', 'repo_name': 'will-i-amv-books/clojure-in-action', 'size': '13247'}