Skip to content

Instantly share code, notes, and snippets.

@gamma235
Last active August 29, 2015 14:07
Show Gist options
  • Save gamma235/c0a62c73edd1fbc6fea9 to your computer and use it in GitHub Desktop.
Save gamma235/c0a62c73edd1fbc6fea9 to your computer and use it in GitHub Desktop.
Implementing OOP in Clojure
;;==== Object Oriented Clojure ====;;
;;;;-------------------------;;;;
;;;; functions & macros ;;;;
;;;;-----------------------------;;;;
;;;;---------| private |--------;;;;
;; general abstraction over override functions
(defn- override [overridee validation-key property-key property-value type-string]
(if (validation-key @overridee)
(swap! overridee assoc property-key property-value)
(println (str "Error: " type-string "-override " "requires a" type-string))))
;;;;-----------| API |----------;;;;
;; adds a clean OO-ish wrapper around the swap! function for adding to or overwriting objects
(defn object-override [object property-key property-value]
(override object :is-object? property-key property-value "object"))
;; " " for adding to or overwriting a class.
(defn class-override [class-name property-key property-value]
(override class-name :is-class? property-key property-value "class"))
;; clean up the syntax around making a class
(defmacro def-class [class-name property-map]
`(def ~class-name (atom (merge ~property-map
{:type "Class"
:class ~class-name
:instantiable? true
:is-class? true
:is-object? false}))))
;; clean up the syntax around making a subclass, add watcher to superclass to allow changes to cascade down
(defmacro def-subclass [subclass super-class property-map]
`(do
(def-class ~subclass (merge (deref ~super-class) ~property-map
{:type "Subclass"
:class ~subclass
:is-subclass? true
:super ~super-class}))
(add-watch ~super-class
:super-inherit
(fn [k# r# old-state# new-state#]
(swap! ~subclass merge old-state# new-state#)))))
;; check to see if class is instantiable and construct a new object if it is.
(defmacro new-object [object-name class-name]
` (let [object-properties# {:type "Object"
:instantiable? false
:is-object? true
:is-class? false}
class-state# (deref ~class-name)]
(if (:instantiable? (deref ~class-name))
(do
(def ~object-name (atom (merge (deref ~class-name) object-properties#)))
(add-watch ~class-name
:obj-inherit
(fn [k# r# old-state# new-state#]
(swap! ~object-name merge old-state# new-state# object-properties#)))
(if (not= class-state# (deref ~class-name))
(do
(remove-watch ~class-name :obj-inherit)
(reset! ~class-name class-state#)
(println "You cannot name an object after a pre-existing class"))))
(println (str "ERROR: " '~class-name " is not an instantiable class")))))
;;==========================================;;
;;-----------------| Tests |------------------;;
;;==============================================;;
;; test: def-class. We use anonymous functions as values, for brevity.
(def-class superhero
{:fly #(println "Whooosh!")
:punch #(println "Whap!")
:has-cape? true
:diet "human food"})
;; test: def-subclass
(def-subclass feline-hero superhero
{:scratch-attack #(println "scrashawww!!!!")
:furry? true})
;; test: panther-man Object should not be instantiable, but feline-hero Class should be
(new-object superhero panther-man)
(new-object panther-man feline-hero)
(:instantiable? @panther-man)
(:instantiable? panther-man)
(:instantiable? @feline-hero)
(:instantiable? @superhero)
;; test: invoking behavoir and testing to see if inheritance happened. We are using keys directly in call position
;; 2 sets of parens for methods, 1 set for "instance variables".
((:punch @panther-man))
((:fly @panther-man))
(:furry? @panther-man)
(:type @panther-man)
;; test: polymorphism on :fly method from the superhero class by overriding it in the panther-man object
(object-override panther-man :fly #(println "HISSS!!!!"))
((:fly @panther-man))
(:is-object? @panther-man)
(:instantiable? @superhero)
;; test: the watchers will cascade changes down to the object if either the class or super-class change state.
(class-override feline-hero :fly #(println "SHEWWUU!!!!"))
(class-override superhero :fly #(println "FLY FLY AWAY!!!!"))
((:fly @panther-man))
(:instantiable? @superhero)
;; test: Assigning a class-name to a new object should not be allowed to happen,
(new-object superhero superhero)
(:instantiable? @superhero)
@superhero
;;--------------------------------------------------------------------------------------------------------------------------;;
;; todo:
;; 1) implement access rights (private/public, etc.), possibly using Java's built in access modifiers
;; 2) Extend it to play nice with interfaces.
;; 3) Consider using meta-data for dealing with type information
;; 4) Find a way to avoid the use of macros
;; 5) provide errors for sub-classes that have name-collisions with the superclasses they extend
;; note:
;; 1) Thread safety should be guaranteed out of the box, because we are using atoms and Clojure's immutable primatives
;; 2) There is probably no practical reason to do this in Clojure.
;; 3) The idea of an "object oriented langauge" is not an either/or proposition if an OO system can be built out of procedures
;; 4) Clojure already has an awesome dispatch using multi-methods/protocols, not to mention full access to Java OO tools
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment