In Clojure, if you want a basic OOP behavior without killing yourself to fully implement Java classes and interfaces in Clojure, the preferred technique is to use records. Sometimes implementing protocols in records suits your needs, mostly due to speed of code execution. However, if you want polymorphism with your records, then multimethods are where it’s at.
Admittedly, multimethods are very flexible, so the technique I’ll show you is one of many techniques you could use. The basic pattern I use for inheritance with records, is to use the derive command with keywords that are kept as a value in the record. For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
(defrecord food [ ^Keyword type ^String name ^String color ^boolean yummy ]) (defn construct-food [name color yummy] (->food ::food name color yummy)) (derive ::fruit ::food) (defrecord fruit [ ^Keyword type ^String name ^String color ^boolean yummy ^boolean needs-peeling ]) (defn construct-fruit [name color yummy needs-peeling] (->fruit ::fruit name color yummy needs-peeling)) |
type is used to track isa relationships of ::food and ::fruit . derive is used to set ::food as the parent of ::fruit . Notice the use of custom constructors to set default values for type in the records.
Now, let’s integrate this with a little polymorphism via multimethods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
(defmulti eat :type) (defmethod eat ::food [params] (str "Enjoy your " (:name params) ".")) (defmethod eat :default [params] (str "Don't eat that.")) (defmulti peel :type) (defmethod peel ::fruit [params] (if (:needs-peeling params) (str "Don't forget to peel your " (:name params) ".") (str "Don't peel your " (:name params) "."))) (defmethod peel :default [params] (str "Your " (:name params) " doesn't need peeling.")) |
Remember, keywords act as functions when dealing with records. So, we use :type to get the values of type in foods and fruits. Then the multimethods sort out the polymorphism needed to call the right functions.
We can use the records and multimethods with the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
(def hamburger (construct-food "hamburger" "multi-colored" true)) (def banana (construct-fruit "banana" "green" false true)) (def apple (construct-fruit "apple" "red" false false)) ;; doing something with our apple (println (eat apple)) (println (peel apple)) (prn) ;; doing something with our banana (println (eat banana)) (println (peel banana)) (prn) ;; doing something with our hamburger (println (eat hamburger)) (println (peel hamburger)) |
The output of the above code should look something like the following.
1 2 3 4 5 6 7 8 9 10 |
Enjoy your apple. Don't peel your apple. Enjoy your banana. Don't forget to peel your banana. Enjoy your hamburger. Your hamburger doesn't need peeling. Process finished with exit code 0 |
And there you have a little polymorphism using records and multimethods.
For convenience here is the entire code sample with namespace and import included.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
(ns myproject.recordexample (:import (clojure.lang Keyword))) ;; creating a food record type with a custom constructor ;; that sets ::food as the default type of the record (defrecord food [ ^Keyword type ^String name ^String color ^boolean yummy ]) (defn construct-food [name color yummy] (->food ::food name color yummy)) ;; creating a fruit record type with a constructor ;; also, setting ::food as the parent of ::fruit (derive ::fruit ::food) (defrecord fruit [ ^Keyword type ^String name ^String color ^boolean yummy ^boolean needs-peeling ]) (defn construct-fruit [name color yummy needs-peeling] (->fruit ::fruit name color yummy needs-peeling)) ;; create an eat multimethod (defmulti eat :type) (defmethod eat ::food [params] (str "Enjoy your " (:name params) ".")) (defmethod eat :default [params] (str "Don't eat that.")) ;; create a 'peel' multimethod (defmulti peel :type) (defmethod peel ::fruit [params] (if (:needs-peeling params) (str "Don't forget to peel your " (:name params) ".") (str "Don't peel your " (:name params) "."))) (defmethod peel :default [params] (str "Your " (:name params) " doesn't need peeling.")) ;; creating foods and fruits (def hamburger (construct-food "hamburger" "multi-colored" true)) (def banana (construct-fruit "banana" "green" false true)) (def apple (construct-fruit "apple" "red" false false)) ;; doing something with our apple (println (eat apple)) (println (peel apple)) (prn) ;; doing something with our banana (println (eat banana)) (println (peel banana)) (prn) ;; doing something with our hamburger (println (eat hamburger)) (println (peel hamburger)) |