I like to experiment with different paradigms and play with different interesting (for me) ideas (some of them turn into posts: times , two ). I recently decided to check if I can write object-oriented code in a functional language.


Idea


I was looking for inspiration from Alan Kay - creator of object-oriented programming.


OOP for me means just messaging; local storage, protection and concealment of states + processes; as well as extremely late binding.

Original:


OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.

I decided that I would be satisfied if I could implement message sending and internal state.


Actually, this is the main problem of the whole idea - the state.


Status


We should not have a state in functional programming at all. How then to change the values ​​in the FP? Usually using recursion (pseudo-code):


function list_sum(list, result) if empty? result else list_sum(tail(list), result + first(list)) list_sum([1, 2, 3, 4], 0) 

In imperative programming, we usually create a variable and constantly change its value. Here we, in fact, do the same by calling the function again, but with different parameters.


But the object needs state and still receiving messages. Let's try this:


function some_object(state) msg=receive_message() next_state=process_message(msg) some_object(next_state) 

It seems to me quite logical. But this code blocks the program. How do I create other objects? How do I send messages between them? Let me quote Alan Kay again:


I saw objects as biological cells and/or individual computers on a network that can only communicate using messages.

This gave me the idea of ​​using concurrency. I called the CDMY0CDMY function "object cycle" and decided to run it in a separate thread. The only secret so far is messaging.


Messaging


For messages, I decided that I could just use the channels (it looks like they’re awful popular in Go). In this case, CDMY1CDMY will just wait until some message appears on the channel (message queue). Sounds pretty easy.


Language


Initially, I wanted to use Haskell, but I don’t know the language, so I would have to deal with lazy calculations, typing and tons of googling for a long time, despite the fact that I just want to create a prototype of my idea. In general, I decided to use Clojure, because it is dynamic, supports interactive programming (which makes life much easier for prototyping and experimenting).


It should be mentioned that it mixes different paradigms, so Clojure maintains its current state:


(def user (atom {:id 1, :name "John"})) @user ; ==> {:id 1, :name "John" } (reset! user {:id 1, :name "John Doe"}) @user ; ==> {:id 1, :name "John Doe"} 

Of course, we will avoid this.


Object


The key concept of object-oriented programming is an object. Things like classes are optional (for example, JavaScript is an OO language, but it doesn't actually have classes; it emulates them using prototypes). Let's start by implementing objects.


What do our objects need? I already mentioned the "object loop" and channels. In addition, we need the CDMY2CDMY function - a message handler.


Clojure has its own channel implementation in the CDMY3CDMY library, so we will use it. But first, we need to think about the data structure for our objects. Actually, nothing complicated:


(ns functional-oop.object (:require [clojure.core.async :as async])) (defn- datastructure [message-handler channel] {:message-handler message-handler :channel channel}) 

Now we just need to add the object loop:


(defn- object-loop [obj state] (let [message (async/<!! (:channel obj)) next-state ((:message-handler obj) obj state message)] (if (nil? next-state) nil (recur obj next-state)))) 

The CDMY4CDMY function simply waits for messages from the channel. The function in: message-handler should in principle take the object itself (self, this), the state and the message itself as arguments.


Everything is ready, we only need to combine all this - create an object:


(defn init [state message-handler] (let [channel (async/chan 10) obj (datastructure message-handler channel)] (async/thread (object-loop obj state)) obj)) (defn send-msg [obj msg] (async/>!! (:channel obj) msg)) 

In this code, we literally run a loop and return a data structure so that we can send messages to the object. The rest of the code can send messages to this object using the CDMY5CDMY function. Function CDMY6CDMY, as you might have guessed, writes something to the channel.


Use objects


This is all great, of course, but does it work? Let's try. I decided to test this by implementing string builder.


String builder is just an object that glues multiple lines together:


builder=new StringBuilder builder.add "Hello" builder.add " world" builder.build # ===> "Hello world" 

Let's try to implement it:


(defn message-handler [self state msg] (case (:method msg) :add (update state :strings conj (:str msg)) :add-twice (let [add-msg {:method :add, :str (:str msg)}] (object/send-msg self add-msg) (object/send-msg self add-msg) state) :reset (assoc state :strings []) :build (do ((:callback msg) (apply str (:strings state))) state) :free nil ;; ignore incorrect messages state)) (def string-builder (object/init {:strings []} message-handler)) 

(this is a slightly modified version of the test I wrote)


In fact, we can treat the message handler as a dispatcher that passes messages to the right methods, depending on which message arrived. Here we have 5 methods.


Let's try running our example with "hello world":


(object/send-msg string-builder {:method :add, :str "Hello"}) (object/send-msg string-builder {:method :add, :str " world"}) (let [result-promise (promise)] (object/send-msg string-builder {:method :build :callback (fn [res] (deliver result-promise res))}) @result-promise) ;; ===> "Hello world" 

The first two lines are understandable without explanation. But what happens next?


Our object lives in another stream and somehow it needs to return some result. How do we get this result? Using callbacks and promises.


Here I just decided to use the callback and set a promise in it. I think this is a very poor design and I should have used promises from the start. But this is just for demonstration, so pffffff.


CDMY7CDMY just pulls the value from the promise. If it is not already installed, then it will wait (blocks the current thread).


Pay attention to the CDMY8CDMY method, it is a little more interesting, because in it, the object sends messages to itself. One of the problems of my architecture is that we cannot call other methods in a method, because the object loop processes only one message at a time. Therefore, for this we will have to do this asynchronously. This is simply a jamb (or feature?) Of this design and it must be borne in mind, otherwise objects can simply hang.


When I tested this method, I did something like this:


1. Вызвать метод :add-twice с аргументом "ha" 2. Вызвать метод :build и проверить, что он равен "haha" 

But the test failed. This is because the CDMY9CDMY message was sent before the CDMY10CDMY method sent CDMY11CDMY messages (don't forget, we have a message queue).


I spent a considerable amount of time trying to figure out what was wrong. This happened because I was not used to parallel programming (my background is Ruby on Rails) and this is a fairly common problem.
Actually, this is one of the reasons why functional programming is becoming more and more popular nowadays - pure functions reduce the chance of such errors. A race condition just happened in my object (two threads were trying to access one piece of memory). Mutability is evil! :)


This was the foundation for our object system. We can build a lot of everything on it. Let's try the classes?


Classes


For me, a class is just a tracing-paper (template) of an object that stores its behavior (methods). And frankly, classes themselves can be objects (for example, as in Ruby). So let's add classes.


First, let's "standardize" how methods are called and executed. I’m too lazy to write, so I’ll just dump this bunch of code right here (garbage):


(ns functional-oop.klass.method (:require [functional-oop.object :as object])) (defn- call-message [method-name args] {:method method-name :args args}) (defn call-on-object [obj method-name & args] (object/send-msg obj (call-message method-name args))) (defn for-message [method-map msg] (method-map (:method msg))) (defn execute [method self state msg] (apply method self state (:args msg))) 

And so. A message to call a method is just a hash consisting of two things: the name of the method and the arguments for it.


Also pay attention to the CDMY12CDMY function. I go a little ahead, but we will give classes methods in the form of a hash. The CDMY13CDMY function defines how objects should run methods: now they accept not messages, but arguments directly, so when we implement methods, we don’t have to think about messages at all.


Message processing is also quite simple:


(ns functional-oop.klass (:require [functional-oop.object :as object] [functional-oop.klass.method :as method])) (defn- message-handler [method-map] (fn [self state msg] ;; Ignore invalid messages (at least for now) (when-let [method (method/for-message method-map msg)] (method/execute method self state msg)))) 

Now let's take a look at what our classes will look like:


(defn new-klass [constructor method-map] (object/init {:method-map method-map :constructor constructor :instances []} (message-handler {:new instantiate}))) 

As you can see, I decided to create classes with objects. I was not obligated to do this, classes could be a more abstract concept, but I decided it was so much funnier.You can go even further and make the CDMY14CDMY function private and create an CDMY15CDMY object that will create classes using the CDMY16CDMY method. This is pretty easy to implement, but I decided not to waste time.


Well, our classes are just objects in which state is methods, a constructor (for initializing class instances), and an array with class instances. The array, in fact, we do not need, but why not.


So what kind of function is CDMY17CDMY? And here she is:


(defn- instantiate [klass state promise-obj & args] (let [{:keys [constructor method-map]} state instance (object/init (apply constructor args) (message-handler method-map))] (update state :instances conj @(deliver promise-obj instance)))) 

When we create a new instance, the constructor is used to get the initial state and the object itself is added to the array mentioned earlier. The object is returned using a promise.


I also added a helper function for synchronized creation:


(defn new-instance "Calls :new method on a klass and blocks until the instance is ready. Returns the instance" [klass & constructor-args] (let [instance-promise (promise)] (apply method/call-on-object klass :new instance-promise constructor-args) @instance-promise)) 

Well, let's try creating a class-oriented string-builder.


(defn- constructor [& strings] {:strings (into [] strings)}) (def string-builder-klass (klass/new-klass constructor {:add (fn [self state string] (update state :strings conj string)) :build (fn [self state promise-obj] (deliver promise-obj (apply str (:strings state))) state) :free (constantly nil)})) (def string-builder-1 (klass/new-instance string-builder-klass)) (method/call-on-object instance :add "abc") (method/call-on-object instance :add "def") (let [result (promise)] (method/call-on-object instance :build result) @result) ;; ==> "abcdef (def string-builder-2 (klass/new-instance string-builder-klass "Hello" " world")) (method/call-on-object instance :add "!") (let [result (promise)] (method/call-on-object instance :build result) @result) ;; ==> "Hello world!" 

Clear!


What's next?


This is just a prototype with a bunch of problems (no error handling, objects may freeze, memory is leaking). But we could realize many more things. For example, inheritance. Or we could follow the path of prototype-oriented programming. Another feature could be a nice DSL for all this, and it could be cool, because we use Clojure.


We also already have mixins right out of the box. Mixins are just hashes with methods that we can use when creating a new class.


Can I do something useful with this?


I made a small demo program - a to-do list (classic). It consists of three classes: list, list item, and command line interface. You can see the code in the repository (link below). I will just say that it was quite simple. This is what the console output looks like:


# add Title: Buy lots of toilet paper # add Title: Make a TODO list # list TODO list: - Buy lots of toilet paper - Make a TODO list # complete Index: 1 # list TODO list: - Buy lots of toilet paper + Make a TODO list # exit 

Conclusion


Uh, that was pretty interesting (for me). Along the way, I was trying to figure out if one could do the same in Haskell. I cannot say for sure, but I think it is possible. Haskell has channels, promises, and concurrency. And even if this were not all, we could expand the idea of ​​the object a little and create them as separate processes and send messages using some RabbitMQ.


For me, the most amazing aspect of programming paradigms is that they are all so different, but absolutely the same. It's not about the language, it's about how the programmer thinks. Languages ​​only allow us to write code in a certain style easier and more productively.


I hope my scribble was not completely boring and maybe you even learned something new :)


A repository with the program and some tests can be found here .


Translation Addendum


The gentlemen at the reddit said that I had reinvented the model of actors and advised me to look at Erlang. My hands have not reached yet, but maybe it will be interesting to you.

.

Source