📜 ⬆️ ⬇️

Lattice inheritance

Inheritance, with seeming simplicity, often leads to complex structures resisting change. Class hierarchies grow like a real forest.
The goal of inheritance is to bind the code (set of methods) to the minimum set of properties of an entity (usually an object) that it provides and which it needs. It simplifies reuse, testing and code analysis. But the sets of properties over time become very large, begin to intersect in a non-trivial way. And in the structure of classes there are mixins and other multiple inheritance.
Making changes in the depth of the hierarchy becomes problematic, you have to think in advance about “dependency injection”, to develop and use complex refactoring tools.

Is it possible to avoid all this? It is worth trying - let the methods be tied to a set of characteristic properties of the object (tags), and the inheritance hierarchy is built automatically according to the nesting of these sets.

Let us develop a hierarchy for game characters. Part of the code will be common to all characters - it is tied to an empty set of properties. The code responsible for their display will be presented in the form of options for OpenGL and DirectX different versions. Something will depend on the character's race, something on the presence and type of magical abilities, and so on. Character tags are primary. They are listed explicitly and not inherited. And the implementation is inherited depending on the set of tags (by nesting). Thus, the ability to fire MANPADS will not be available to the kangaroo, because it was inherited from the infantryman.
')
The idea of ​​this approach was proposed by Dmitry Kim. The author did not embody it in the code, I will try to correct this omission.
Implementing this approach on Clojure, as usual, on github .

The implementation of this method of inheritance is made on top of the Clojure system of generalized functions (multimethods).
Each multimethod defined with defmulti is associated with a hierarchy and dispatching function, which maps arguments to elements (or an array of elements) of the hierarchy. Usually the elements of the hierarchy are data types, but in their hierarchies you can also use “keywords” and “symbols”, which will mark data related to the desired type.
The specific implementation of the method for the hierarchy element is specified using defmetod.
It looks like this:
(use 'inheritance.grid) (def grid (make-grid-hierarchy)) (defmulti canFly "  " (grid-dispatch1) :hierarchy #'grid) (defmulti canFireball "   " (grid-dispatch1) :hierarchy #'grid) (defmulti canFire "  " (grid-dispatch1) :hierarchy #'grid) (defmethod canFly (get-grid-node {} #'grid) [p] false) ;     (defmethod canFly (get-grid-node {:magic :air} #'grid) [p] true) ;    -  (defmethod canFly (get-grid-node {:limbs :wings} #'grid) [p] true) ;   (defmethod canFireball (get-grid-node {} #'grid) [p] false) ;      (defmethod canFireball (get-grid-node {:magic :fire, :limbs :hands} #'grid) [p] (> (:mana p) 0)) ;       - ,   . (defmethod canFire (get-grid-node {} #'grid) [p] false) ; , ,     (defmethod canFire (get-grid-node {:limbs :hands} #'grid) [p] true) ;     (defmethod canFire (get-grid-node {:magic :fire} #'grid) [p] (> (:mana p) 0)) ;       (defmethod canFire (get-grid-node {:magic :fire, :limbs :hands} #'grid) [p] true) ;     - Clojure   ,      (def mage ((with-grid-node {:magic :fire, :limbs :hands :race :mage} #'grid) {:mana 100, :power 5})) (def barbar ((with-grid-node {:magic :none, :limbs :hands :race :human} #'grid) {:power 500})) (def phoenix ((with-grid-node {:magic :fire, :limbs :wings :race :mage} #'grid) {:mana 200, :power 4})) (def elf ((with-grid-node {:magic :air, :limbs :hands :race :elf} #'grid) {:mana 300, :power 13})) (canFire elf) ; true (canFireball elf) ; false (canFly elf) ; true (canFly mage) ; false (canFire mage) ; true 


How does it work:
First you need to create a hierarchy - this will be the usual Clojure hierarchy, with a table displaying a set of tags (in the form of an associative array) to a member in the hierarchy. The table is initially empty and is stored in the meta-information of the hierarchy object.
 (defn make-grid-hierarchy "   " [] (let [h (make-hierarchy)] ;    (with-meta h (assoc (or (meta h) {}) :grid-hierarchy-cache {})))) ;       


Each set of tags used must be registered in the hierarchy - a symbol is created for it and included in the correct place of the hierarchy, and the corresponding entry in the table is added so that this symbol can be found. Calculating the correct place in the hierarchy is the basis of this method of managing inheritance.
 (defn register-grid-node "     " [ho] (let [nl (get (meta h) :grid-hierarchy-cache {})] (if-let [s (nl o)] ;       [hs] ;        (let [s (symbol (str o)) ;   -    hn (reduce (fn [h [tr n]] ;     (if (and (subobj? tr o) ;          (not (isa? hsn))) ; Clojure      , ;     (derive hsn) (if (and (subobj? o tr) (not (isa? hns))) ;       (derive hns) h))) h nl)] [(with-meta hn ;         (assoc (or (meta h) {}) :grid-hierarchy-cache (assoc nl os))) s])))) ;        


Now we need to learn how to associate a type from a certain lattice site, given by a set of tags, with data that we consider to belong to this type.
 (defn with-grid-node " ,      " [nh] (let [s (get-grid-node nh)] (fn [v] (with-meta v (assoc (or (meta v) {}) :grid-node s))))) 

To avoid repeated searches on the node table, this function gets the character corresponding to the node and returns a closure that adds this character to the meta information of its argument.

Dispatch functions are simple.
 (defn grid-dispatch "     " [] (fn [& v] (vec (map (fn [a] (:grid-node (meta a))) v)))) (defn grid-dispatch1 "    " [] (fn [v & _] (:grid-node (meta v)))) 


I have already tried to implement such inheritance in Common Lisp. But I do not know the MOP device, and that implementation is not built into CLOS and is not very efficient.

Source: https://habr.com/ru/post/242649/


All Articles