Poor man's objects?

I was toying around a bit with trying to use functions as poor man’s object. And this is the best immutable way I found for now:

(defn counter
 ([] (counter {:count 0}))
 ([state]
  {:inc 
   (fn []
    (counter (update state :count inc)))
   :double
   (fn []
    (counter (update state :count #(* % 2))))
   :count
   (fn []
    (:count state))}))

((:count ((:double ((:inc ((:inc (counter)))))))))
;;=> 4

Seems pretty nice, except for having to double call each thing with parenthesis. I thought that could be something a macro makes a bit nicer.

Anyone has something better?

2 Likes

This seems a lot like what Brian Marick has in his Functional Programming for the Object-Oriented Programmer book. And I thought The Joy of Clojure had something similar (but I can’t find it right now).

Brian has a send-to function that would turn that code into:

(send-to (send-to (send-to (send-to (counter) :inc) :inc) :double) :count)

but at least that is amenable to threading:

(-> (counter) (send-to :inc) (send-to :inc) (send-to :double) (send-to :count))

Interesting, but a bit cumbersome, so I made these modifications:

(defn Counter
  ([state]
   {:new
    (fn new-counter
      ([] (new-counter 0))
      ([count]
       (Counter {:count count})))
    :inc
    (fn []
      (Counter (update state :count inc)))
    :double
    (fn []
      (Counter (update state :count #(* % 2))))
    :set
    (fn [new-count]
      (Counter (assoc state :count new-count)))
    :count
    (fn []
      (:count state))}))

(defmacro =>
  [obj & commands]
  (reduce
   (fn[acc e]
     (let [e (if (list? e) e (list e))
           cmd (first e)
           args (next e)]
       (apply list (list cmd acc) args)))
   `(cond
      (fn? ~obj)
      (list ~obj)
      :else
      ~obj)
   commands))

(defn =new
  [obj & args]
  (apply (:new (obj nil)) args))

(=> (=new Counter)
    (:set 100)
    :count)
;;=> 100

(=> (=new Counter 10)
    :inc
    :inc
    :double
    :count)
;;=> 24

(let [counter (=new Counter 10)]
  (=> counter :inc)
  (=> counter :count))
;;=> 10
1 Like

Pardon the silly question, but at this point, why not just use records and protocols?

(declare counter)

(defprotocol ICounter
  (-inc [counter])
  (-double [counter])
  (-count [counter]))

(defrecord Counter [cnt]
  ICounter
  (-inc [this] (counter (inc cnt)))
  (-double [this] (counter (* cnt cnt)))
  (-count [this] cnt))

(defn counter
  ([] (counter 0))
  ([n] (->Counter n)))

(-> (counter) -inc -inc -inc -double)
2 Likes

Reminds me of the part in SICP where you create an object system from higher order functions, although they model it through mutable state.

2 Likes

Yup this was my same thought. Or, if you prefer, as of clojure 1.10 you can do:

(defprotocol ICounter
  :extend-via-metadata true
  (-inc [counter])
  (-double [counter])
  (-count [counter]))

(defn counter
  ([] (counter 0))
  ([cnt]
   (with-meta
     {:counter cnt}
     {`-inc    (fn [this] (counter (inc cnt)))
      `-double (fn [this] (counter (* cnt cnt)))
      `-count  (fn [this] cnt)})))

(-> (counter) -inc -inc -inc -double)

There might be reasons you want the messages passed between your “objects” to be first class “things”. You might want to record history of what is invoked on an object, or otherwise intercept the invocation.

I’ve been re-watching all of the Rich Hickey talks lately and the intent behind the type of thing you are describing is one of the motivations behind core.async. [https://github.com/matthiasn/talk-transcripts/blob/master/Hickey_Rich/CoreAsync.md]. If you want your messages to be first class things, you probably will benefit from communicating the messages via queues.

3 Likes

This is almost philosophical but I’m not sure there’s a difference between message passing and how protocols work. Method dispatch is just table lookup/
In any sense, if you want to capture the invokation you can always add an audit trail to the object’s metadata of all the calls it had gone through.

This is more me fooling around then anything serious. I was wondering if there’d be a way to implement simple objects using functions, but without closing over mutable data.

That said, there are some minor differences between objects and protocols/records. One difference is that the object encapsulates the data strongly.

If you look at my code, it’s not possible for anyone else to mess with the data fields through any other means but the object’s methods.

In my case, since I’m using immutable data it doesn’t really matter. But if they were mutable it would. Strong encapsulation would give you some guarantees that you can trust no one else will mutate your data under your feet.

Another one is that the behavior and the data are tightly coupled together with objects. So for example if you want to modify the data, it’s much more obvious what is the total set of methods over it which you need to refactor accordingly.

On that same line of thought, if you wanted auto-complete for example, because the relationship between the data and it’s set of methods is so strong, it is very easy to list out for any object all the possible methods. For example in my code, one can just call keys on the object and you get the list of possible operations. So it be trivial to add auto-complete to my => macro in some editor for example.

None of those are killer features, I’m in no way advocating for OOP :yum:, just saying there are some minor differences when you start to go deep into the details.

But yes, for real Clojure, I’d probably not even use protocols I’d just go:

(defn make-counter
  []
  0)

(defn inc [counter]
  (inc counter))

(defn double [counter]
  (* 2 counter))

(-> counter inc inc double)

Since I’m not making use of polymorphism, protocols give you no additional benefit then just plain functions over a simple data-structure as I just wrote.

There’s concrete minor differences here too. In fact, it isn’t philosophical. With message passing, you exchange serialized data which you copy and send, that means you can’t share a pointer to some object in memory for example, you need to serialize the object somehow and pass it over as a message which can be deserialized by the receiver.

With protocols and method dispatch, you don’t serialize anything, you pass reference around and share the in-memory objects between caller and callee.

Wouldn’t you say the only motivation for serialization and copying are mutability? I.e. when everything is immutable, there’s no inherent problem with passing a reference to the message, which is itself deserialized data, instead of a copy.
With regards to encapsulation you’re completely right. There’s also the matter of where the object’s dispatch data is located. In the object itself or in a global environment which all instances share.

I think the clojure.zip implementation has a hint of object-orientation the way it attaches “methods” to every node in the zipper and some of the operations attach a bit of state (^:changed?) too.

I wrote a bit about this awhile ago:

3 Likes

From @jackrusher’s article:

It’s object-oriented because the authors decided that those principles were the best way to solve the problem at hand, and thus it admirably demonstrates the mechanism of OO.

This really speaks to me. I didn’t learn OO when I learned Java in the university. Then, I learned setters, getters and public static void main(String[] args).

I learned OO when I was trying to make sense of a messy problem with Python. I had just used functions in modules, but I was having trouble with the ordering – I needed some “load” step, and it was completely unclear which order I’d have to run my functions in. I was happy to discover that classes with static methods for “load and validate” was a neat solution.

1 Like

I would also like to recommend the talk The Object-Oriented/Functional symmetry: theory and practice by Gabriel Scherer. Gabriel Scherer is part of the team that develops Ocaml. It presents another outside-in perspective on OO.

1 Like

That’s a nice talk. I’m glad he gets into the Expression Problem a bit. :slight_smile: Where he talks about having a closed list of cases that are set in stone, that’s the same thing I was getting at in this comment (hopefully linked directly):

1 Like

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.