Skip to content

Instantly share code, notes, and snippets.

@marick
Created July 6, 2012 19:01
Show Gist options
  • Save marick/3062098 to your computer and use it in GitHub Desktop.
Save marick/3062098 to your computer and use it in GitHub Desktop.
;; Is it functional programming's immutability that makes it a bad match for
;; object-oriented style, particularly inheritance?
;; For /Functional Programming for the Object-Oriented Programmer/
;; (http://leanpub.com/fp-oo), I've implemented something like the
;; Ruby object system in Clojure. Objects are built out of maps
;; (hashes) with stylized "metadata". Here's a `Point` object:
{:y 6, :x 4, :__class_symbol__ 'Point}
;; Classes are also objects, but they have additional metadata, such as
;; their superclass and a map of methods that apply to their instances.
;; Sending a message looks like this:
user=> (send-to Point :new 1 2)
{:y 2, :x 1, :__class_symbol__ Point}
user=> (def point (send-to Point :new 1 2))
user=> (send-to point :shift 1 2)
{:y 4, :x 2, :__class_symbol__ Point}
;; Because Clojure has immutable maps, `:shift` produces a new `Point`.
;; Here's one implementation of `:shift`. It's a fairly typical OO
;; one, I think. Notice, for example, how it takes care to send
;; `:new` to the right class, which may be a subclass, rather than
;; hardcoding `Point`.
;;
:shift
(fn [this xinc yinc]
(let [my-class (send-to this :class)]
(send-to my-class :new
(+ (:x this) xinc)
(+ (:y this) yinc))))
;; I use "direct instance variable access" rather than getters mainly because
;; `(send-to this :x)` is too annoying. Except for that, this code sticks closely
;; to the illusion that we have conventional objects, rather than maps.
;; But suppose we acknowledged the reality of maps. In Clojure, you merge
;; two maps with `merge`. There's a related function, `merge-with` that takes
;; an additional argument: the function to be applied when the two maps have
;; conflicting keys. Using that, we can write a much simpler `shift`:
:shift
(fn [this xinc yinc]
(merge-with + this {:x xinc, :y yinc}))
;; This is better. But there's more. Consider a subclass of `Point` called
;; `ColoredPoint`. It is constructed with an extra argument: the color of the point.
;; Like this:
(send-to ColoredPoint :new 1 2 'red)
;; It seems to make sense to shift colored points, but the superclass method
;; doesn't work in the subclass (because `:shift` only calls `:new` with two
;; arguments). We could write a version of new in the subclass:
:shift
(fn [this xinc yinc]
(let [my-class (send-to this :class)]
(send-to my-class :new
(+ (:x this) xinc)
(+ (:y this) yinc)
(:color this))))
;; But this duplication seems wrong. We should use the superclass. But
;; doing that is annoyingly cumbersome. We might make a point,
;; populating it correctly, shifting it, then using the resulting Point's
;; x and y values to make a ColoredPoint. Blech.
;; Now note that the hash-aware version of `:shift` works perfectly
;; fine in ColoredPoint: no need for overriding it in the subclass.
;; This seems like an argument in favor of using maps instead of objects: additional
;; information is easy to work with.
;; You might object that this is a side-effect of Clojure's immutable maps. In Ruby,
;; `shift` would probably mutate the point:
class ColoredPoint < Point
def shift(xinc, yinc)
@x += xinc
@y += yinc
end
end
;; In that case, `shift` would work just fine in subclasses.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment