FP and OOP — Writing a simple functional object system

Rain
9 min readDec 16, 2018

Introduction

In this article we will iteratively implement a simple object system suitable for object-oriented programming (OOP) in a functional programming (FP) style. The goal of this exercise is two-fold:

  1. By implementing the functionality commonly used in OOP from scratch we will gain a deeper understanding of how it works and how simple the underlying concepts are.
  2. The resulting system will be built using functional design patterns and lend itself heavily to be used in FP. Thus it can serve as an example of how OOP and FP are not mutually exclusive paradigms, but orthogonal ones.

This system will be written in Scheme. However, we will make no use of macros, continuations and similar advanced techniques. Additionally the code shown will be accompanied by explanations. Thus this article should be understandable even if you don’t know Scheme. You can try out the listed code in either a local interpreter such as Chez Scheme or by using an online REPL.

To start we will first establish our requirements by looking at what exactly OOP defines. After that we will implement a simple object and slowly work our way to a more complete and generic way to represent and instantiate objects until we have a complete object system.

What is OOP?

If you ask three programmers to define OOP, you will most likely get five different answers. There are a multitude of different approaches and between them large differences in terminology as well as functionality. To illustrate this, here is a short list of differences:

  • Class-based versus prototype-based
  • Inheritance versus delegation
  • Single-dispatch versus multi-dispatch
  • Method calls versus message-passing

So clearly there’s a lot of confusion here. But if we ignore the minutiae of these concepts and instead ask what is common between object systems, a few fundamental properties quickly emerge.[1]

The first of those is encapsulation. Meaning that when we interact with an object, we interact with its public interface. Its internals are local to only the object, and we can’t reference them (except with meta-programming).

Then there is inheritance and/or delegation. With inheritance we can define a new object or class based on an existing object or class. Delegation allows us to pass a method call or message from one object to another. Both of these serve the same purpose: with them we can use the behavior of an object or class as basis for a different one.

And lastly we’ll note polymorphism. In the context of OOP this means subtyping especially. Subtyping enables a relationship between two datatypes such that one (the subtype) can be safely used where-ever the other (the supertype) is expected. In object systems this is commonly implemented via inheritance or delegation — A derived object can safely be used in place of its base object.

Defining our first object

All this might look a bit complicated on the first glance. That look is deceiving however, and you might be pleasantly surprised by just how simple the underlying concepts are and how trivial our implementation is. With all that out of the way, let us now now start to explore how a simple object could look like in our system.

Keeping it simple

For the sake of simplicity this first iteration will consist of a single object that eschews most of these principles and focuses on more practical concerns. We will implement the following:

  • A way to instantiate a counter object with an initial integer value as state. This state will be encapsulated and immutable to keep in line with OOP and FP principles.
  • The instantiated object will be able to receive messages and answer them.
  • If it receives the + message it will return a new counter with the initial value set to the sum of its state and the messages argument.
  • If it receives the message it will do the same as with +, but with the difference instead of the sum.
  • It it is unable to answer a message, it will throw an error.

Designing counter%

With these specifications in mind we can now reason about the concrete algorithm and Scheme forms to use in our program. We will find that — apart from a bit of trivial code — our design rests on how we choose to implement two fundamental concepts: Objects and messages.

Thinking a bit more about objects it becomes apparent that they are very similar to functions: both encapsulate internal variables and allow us to refer to a group of expressions as a singular unit.

Recognizing this, a simple and intuitive implementation of objects and messages emerges: lexical closures and function calls, respectively.

But what is a lexical closure? Without going into too much detail here’s a short definition:

  • A lexical closure is a function instance whose free variables are bound in the environment it was created in[2].
  • Free variables refers to the set of variables a function has access to that are neither defined inside it, nor parameters to it.
  • An environment is a set of mappings from variables to their values.

Those properties allow a design pattern[3] in which we assign some variables and then reference them inside a lexical closure we subsequently create. This results in a closure that combines encapsulated values with behavior to access them.

In practical terms this means we can use lexical closures as objects the following way:

  1. To start off we will use define to define a procedure named counter% which takes one parameter, i. This is our objects initial state. It should be noted that every invocation of counter% will have its own environment with a fresh binding for i in it.
  2. That procedure will return the result of a lambda expression, which is a lexical closure. So any reference to the variable i inside it will return the value of i in the environment the current call to counter% provides.
  3. This closure will take two parameters: msg, the message to answer, and arg, the argument to that message.
  4. To execute different code based on which message the object received we will use case.

Some code, finally!

Without further ado let’s take a look at the resulting code:

We can use counter% like this:

(define c (counter% 2))
(c 'get)
> 2
(define c2 (c '+ 3))
(c2 'get)
> 5

As we can see our object behaves like a functional data structure: Rather than mutating its state, it returns a fresh instance with an updated value.

A generic way to represent and instantiate objects

Our first foray into the world of objects rewarded us with a pretty simple implementation of an object that works well enough and already follows one of our design goals: encapsulation.

But of course we don’t want to define each and every object we use from the ground up. Rather we want to be able to generalize counter% into a procedure that will take an arbitrary number of field definitions. These would then consist of a message and code to answer it.

For this we will use a list with two elements. While the first is simply the message, the second will be a lexical closure which takes two parameters:

  • A list consisting of all arguments to the message.
  • And a way for the object to refer to itself. In most object-oriented languages this is usually done with the keyword self or this.

Now we might define counter% as procedure that calls the generalized make-object with three of these receivers, corresponding to the messages our original counter implemented:

For understanding, car is a procedure that simply returns the first element of a list or pair— here used to get the first and only argument to + and -.

As you can see we also use (s 'get) to refer to i. This makes no practical difference to using i directly, but it does show off self-reference.

Now let’s define make-object. This is another simple procedure, albeit it does use some new forms:

  1. In (define (make-object . recvs) ...) you might notice a period preceding the parameter. This signifies that make-object is a procedure that takes a variable amount of arguments and stores them as list in recvs.
  2. We then use letrec to introduce some local variable bindings — specifically the helper functions lookup, self and call.
  3. lookup fills the same role that case earlier did: it takes a message and either returns a procedure to answer it or signals that the object is unable to answer said message.
  4. self is what we will supply to the answer procedures to allow them self-reference.
  5. call takes a receiver list, extracts the answer function and calls it with the given arguments and self.
  6. Now we once again return a lexical closure. The difference is that this one takes at least one argument, the message msg, and stores any additional ones in the argument list, args.

When we now use the new counter%, it will create an object instance that is functionally identical with the one the old counter% created — the only difference being that we may now send a message with any number of arguments, though excess ones will be quietly ignored.

Delegation and Polymorphism

This is all well and good, but there are still a few important pieces missing. If you recall, our definition of OOP included inheritance/delegation and polymorphism in addition to encapsulation. Luckily these are trivial to add to our existing make-object!

For the sake of simplicity we will implement forwarding — which is very similar to delegation— to allow a prototype-based style of OOP. This is because inheritance would require us to extend our object system with the concept of classes.

So which additions do we need?

  • make-object needs the additional parameter super%, to which our sub-object will forward any messages it can’t answer.
  • lookup needs to be modified to allow forwarding. If there is no super-object, it should still signal unanswerable messages with an error.

Additionally every object needs two additional receivers:

  • super, which can be used to explicitly forward to the super-object instead of looking up a receiver in the sub-object first. This is similar to the super or parent keyword in many languages.
  • And lookup, which looks up the receiver to a specific message and returns it.

The way this all comes together is simple: To forward a message we will send lookup to the super-object and then call the returned receiver.

The difference between forwarding and delegation is this: By using call in the sub-object, the receiver will use that sub-objects self — and thus its context. For delegation on the other hand we would evaluate the receiver with the super-object self.

The reason for this choice is simple: These two concepts are very similar, but forwarding more closely mimics how self or this works in popular object-oriented languages the reader might be familiar with.

With these changes our definition of make-object now looks like this:

Finally let us define and use a few objects more complex than counter. This way we can test if that simple procedure really forms a complete object system that implements encapsulation, delegation/forwarding and polymorphism.

(define n (point3d% 2 3 5))
(define m (point% 2 3))
(m 'eql? n)
> #f
(n 'posn)
> (2 3 5)
(n 'sum)
> 10
  • Regarding encapsulation we note that the internal state of our object — the integers 2, 3 and 5 — is not directly accessible. We may use posn, but it is just an interface designed by the object.
  • Looking towards delegation/forwarding, we can see that it works as intended: eql? gets forwarded to the super-object, while posn and sum are answered by the sub-object — the latter using super to explicitly reference the super-objects implementation.
  • Finally polymorphism: As our use of eql? shows, a point3d may safely be used where-ever a point is expected.

Discussion

In around 10 lines of code we have written a object system. For this we only used basic forms common to modern programming languages — thus it is trivial to translate[4]. This simplicity by itself should dispel any doubts about the inherent complexity of object systems.

Our design also shows how well OOP and FP work together: We forego any thoughts about mutating object state in favor of using objects as immutable data structures coupled with functionality. This approach isn’t arbitrary, but the logical conclusion to an object system implemented in functional primitives and design patterns.

So by these measures this article succeeded in fulfilling its goals. There are some things that could have been differently though:

  • By choosing Scheme rather than a more popular language the article might have been less accessible for many. However, Scheme has the advantage of not offering any built-in object-orientation — thus it is obvious to the reader that our implementation is truly “from scratch”.
  • Implementing forwarding instead of delegation lead to increased complexity, which might make the code less accessible. On the other hand, it does show just how many approaches there are to object orientation.
  • We used a name and a list of arguments to represent a message. It would have been more elegant — but more complex as well — to implement it as object .

As final remark those things are left as exercise to the reader: Translation into a different language, as well as implementing delegation and messages as objects.

[1] Another interesting definition of object-orientation is the one Alan Key gives in “The Early History of Smalltalk”.

[2] In contrast to dynamic closures, whose free variables are bound to the environment the closure is called in.

[3] Often called “Let over Lambda”.

[4] To demonstrate this: a JavaScript translation.

--

--

Rain

Let's watch the end of the world together. Essays on various topics, which may include philosophy, computer science, PLT, harm reduction, sociology.