Clojure multimetódus és hierarchia

A Wikipédiából, a szabad enciklopédiából
Ugrás a navigációhoz Ugrás a kereséshez

A Clojure nem a hagyományos objektumorientált módon készít adattípusokat, hanem mindig felépít egy nagy függvénykönyvtárakat hozzá. Prioritásának tekinti a futás idejű polimorfizmust, így lehetővé teszi flexibilis és bővíthető rendszerek kialakítását. Ezt a fajta polimorfizmust egy multimetódus rendszeren keresztül teszi lehetővé. Ez a rendszer lehetővé teszi az argumentumok típusainak, értékeinek, attribútumainak, metaadatainak és kapcsolatainak továbbküldést (dispatch).

A multimetódus kombinációja egy dispatcher funkciónak és egy vagy több másik metódusnak.  Multimetódust definiálni a defmulti kulcsszóval lehet. Multimetódus definiálásánál kötelező megadni a dispatching funkciót. Ez a funkció lesz használva a multimetódus argumentumain, hogy előállítson egy dispatching értéket. Ezután a multimetódus megpróbálja megkeresni ehhez az értékhez tartozó metódust vagy azt az értéket, ahonnan a dispatching érték származott. Ha a metódus definiálva lett (defmethod kulcsszóval), akkor meg lesz hívva az argumentumokkal. Ezután ez lesz a metódus hívás értéke. Amennyiben nem talál olyan metódust, ami a dispatching értékhez tartozik, akkor az alap dispatching értéket használja. Az alap dispatching értékhez tartozó metódust keresi meg, és használja fel, ha létezik. Ha ilyen sincs, akkor a hívás hibát ad.

A multimetódus rendszer API-ja a következő:

-defmulti: új multimetódust készít

-defmethod: egy dispatching értékkel összefüggésben álló metódust készít

-remove-method: törli a dispatching értékhez tartozó metódust

-prefer-method: több lehetséges metódusok között készít egy rendezést, amely eldönti, hogy melyiket célszerűbb használni

A származtatás osztályértékekre Java-öröklődés vagy a Clojure ad-hoc hierarchiarendszer kombinációja alapján történik. Ez a hierarchiarendszer támogatja a származtatását kapcsolatoknak nevek között, és kapcsolatoknak osztályok és nevek között. Ezeket a kapcsolatokat a derive funkció készíti el, valamint az isa? funkciót, ami teszteli a kapcsolatok létezését.

Hierarchikus kapcsolatok a következővel adhatók meg: a gyerek és a szülő lehet szimbólum vagy kulcsszó, de kötelezően névtér meghatározottnak kell lennie (namespace-qualified):

Megjegyzés: a ::reader syntax, ::keywords névtereket valósítanak meg.

::rectangle
-> :user/rectangle

Az alapvető kapcsolatépítő a derive.

(derive ::rectangle ::shape)
(derive ::square ::rectangle)

parents (szülők) / ancestors (ősök) / descendants (leszármazottak) és isa? a hierarchia lekérdezésére szolgál

(parents ::rectangle)
-> #{:user/shape}

(ancestors ::square)
-> #{:user/rectangle :user/shape}

(descendants ::shape)
-> #{:user/rectangle :user/square}

(= x y) implementálja (isa? x y)

(isa? 42 42)
-> true

isa? a hierarchia rendszert használja

(isa? ::square ::shape)
-> true

Az egyetlen lehetőség, hogy valamit gyerekké tegyünk, Java-öröklődés útján lehetséges. Így használható egy osztály gyerekként is. Ez lehetővé teszi új rendszerek ráhelyezését már létező Java-osztályhierarchiára.

(derive java.util.Map ::collection)
(derive java.util.Collection ::collection)

(isa? java.util.HashMap ::collection)
-> true

isa? teszteli az osztály kapcsolatait

(isa? String Object)
-> true

szülők (parents) / ősök (ancestors) is szintén (de leszármazottak (descendants) nem, mivel ezek nyílt halmazok)

(ancestors java.util.ArrayList)
-> #{java.lang.Cloneable java.lang.Object java.util.List
    java.util.Collection java.io.Serializable
    java.util.AbstractCollection
    java.util.RandomAccess java.util.AbstractList}

isa? vektorokkal dolgozik úgy, hogy meghívja az isa?-t a megfelelő elemekre:

(isa? [::square ::rectangle] [::shape ::shape])
-> true

isa? alapú küldés (dispatch)[szerkesztés]

A multimetódusok isa?-t használnak = helyett amikor tesztelik a dispatch értékek egyezését. Fontos, hogy az isa? első tesztje = teszt, tehát a pontos egyezések működnek.

(defmulti foo class)
(defmethod foo ::collection [c] :a-collection)
(defmethod foo String [s] :a-string)

(foo [])
:a-collection

(foo (java.util.HashMap.))
:a-collection

(foo "bar")
:a-string

A prefer-method egyértelműsítésre szolgál, akkor, ha több, egyforma dominanciával rendelkező egyezés létezik. Egyszerűen deklarálható minden multimetódushoz, hogy melyik érték részesüljön előnyben.

(derive ::rectangle ::shape)

(defmulti bar (fn [x y] [x y]))
(defmethod bar [::rectangle ::shape] [x y] :rectangle-shape)
(defmethod bar [::shape ::rectangle] [x y] :shape-rectangle)

(bar ::rectangle ::rectangle)
-> Execution error (IllegalArgumentException) at user/eval152 (REPL:1).
   Multiple methods in multimethod 'bar' match dispatch value:
   [:user/rectangle :user/rectangle] -> [:user/shape :user/rectangle]
   and [:user/rectangle :user/shape], and neither is preferred

(prefer-method bar [::rectangle ::shape] [::shape ::rectangle])
(bar ::rectangle ::rectangle)
-> :rectangle-shape

A fenti példák a globális hierarchiát használják, ami a multimetódus rendszer által van használva. make-hierarchy-val különálló hierarchiák is kiépíthetők. A fenti funkciók opcionális hierarchiát vehetnek fel első argumentumként.

Ez a rendszer nagyon hatékony. Egy út, hogy megértsük a kapcsolatokat a Clojure multimetódusok és a tradicionális Java stílusú egyszeres dispatch között, hogy az egyszeres dispatch olyan, mint a Clojure multimetódus, aminek a dispatch funkciója meghívja a getClass-t az első argumentumon, amelynek a metódusai kapcsolatban állnak azokkal az osztályokkal. A Clojure multimetódusok nem erősen kötöttek osztály/típusra nézve, bármilyen  argumentum tulajdonságon alapulhatnak, több argumentumon, csinálhatnak validációt rajtuk, és eljuthatnak hibakezelő metódusokig.

Megjegyzés: Ebben a példában a :Shape kulcsszó dispatch funkcióként van használva, mivel a kulcsszavak a map-ek funkciói.

(defmulti area :Shape)
(defn rectangle [width height] {:Shape :Rectangle :width width :height height})
(defn circle [radius] {:Shape :Circle :radius radius})
(defmethod area :Rect [r]
    (* (:width r) (:height r)))
(defmethod area :Circle [c]
    (* (. Math PI) (* (:radius c) (:radius c))))
(defmethod area :default [x] :oops)
(def rect (rectangle 4 13))
(def circ (circle 12))
(area rect)
-> 52
(area circ)
-> 452.3893421169302
(area {})
-> :oops