Last active
August 29, 2015 14:07
-
-
Save gamma235/c0a62c73edd1fbc6fea9 to your computer and use it in GitHub Desktop.
Implementing OOP in Clojure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;;==== 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