When talking about mainstream programming languages, we often put them into two major buckets: object oriented programming and functional programming. But these programming paradigms are not oil and water. In this article I'll be blending the two to demonstrate a technique in Elixir.

These resources shed light on OOP in Elixir, but they don't demonstrate the building blocks that comprise a working model. In this article, we will build an object system in Elixir from scratch and as bare bones as possible. To do this, all of the Elixir code will be based off Ruby examples. The goal is to highlight some of the core concepts of Elixir and liken them to core concepts of Ruby. (Some familiarity with Elixir is assumed.)

Though we'll build a working model, it's not meant to be an ideal object system nor am I advocating for this style of programming. In fact, the Erlang community relies on vastly different patterns. This is merely an exercise in learning about message passing and state management in Elixir. There are likely several ways to accomplish the same effect and I'd love to hear about other techniques in the comments.

Primary Concepts in Object Orientation

Object orientated and functional programming have coexisted since the dawn of modern computing. Functional programming originated from mathematics and was naturally conceived first. Object orientation came shortly after with Simula, whose goal was to make state management more straightforward.

Most literature on object orientation rehashes the same core programming concepts: state, behavior and polymorphism. In Ruby, this translates to instance variables, methods, and duck typing. These rudiments can be expanded to other OO concepts, like encapsulation and the SOLID principles.

But modern object orientated languages are unnecessarily limiting. It's almost as though they're missing the forest for the trees. The great techniques in object orientation can be written in a functional style, should you want them, while retaining purity and safe concurrency, which we usually want.

A Basic Object

An object conjoins state and behavior. It encapsulates state internally and exposes behavior externally. Encapsulation is important because it is an effective means of organizing and controlling state. Behavior is the means by which we change state.

Consider the following example of a car in Ruby. The car has a position, x, and the ability to drive forward. When it drives, it increments x by 1.

Notice in the above class, when #drive is called, it prints the name of the class, its color attribute, and the change in the state of x.

The variable @attrs is the encapsulated state. The #drive method is the behavior. What's beautiful about this class is that once instantiated, we can just call the #drive method and we don't have to think about the internal state of x. Let's do that now.

We call #drive twice and the internal state of x changes. Encapsulation is simple and powerful.

We can write this "class" and this "object" in Elixir. The primary difference is how we encapsulate state and invoke behavior. Instead of encapsulation being a first class citizen like it is in Ruby, we use recursion to represent state. Instead of calling methods like we do in Ruby, we pass a message.

The new/1 function is named "new" to mirror Ruby's .new method, but this could be named anything. This function creates a new Elixir process using spawn_link/1. We can consider spawn_link/1 the equivalent to Ruby's special .new method. By spawning a new process, we have now created an area to encapsulate state.

If you're not familiar with Elixir processes, you can think of them the same way you think of operating system processes - in fact, they're modeled after OS processes but are extremely lightweight and significant faster. They run independent of each other, have their own memory space that doesn't bleed, and can fail in isolation.

Inside the newly created process, the default attributes are merged with the attributes passed as an argument. Then, the recursive function run/1 is called.

# elixir
Map.merge(@default_state, state) |> run

The run/1 function is the core of our "object" and the state is recursively passed to run/1 over and over, encapsulating the state as an argument to the function. When we want to update the state, we call the run/1 function with the new state.

One key component of getting this to work is the call to the receive function. When receive is called, it will block the current process and wait until the process receives a message. Remember, this code is running in a new process all to its own. When a message is passed to the process, it will unblock itself and run the code declared in the proceeding block.

This proceeding block calculates a new state by incrementing x into a new variable, updating x in the map that represents the state, and then recursively calling the run/1 function. After calling run/1 recursively, the process again blocks on receive. It continues to recursively do this indefinitely until the run/1 function decides not to call itself anymore. When we no longer recurse, the process dies and the state is garbage collected. (This non-recursive case is not represented in this code.)

Let's see again what "instantiation" and message passing looks like. The built-in function, send/2 is used to send a :drive message to the process twice.

In the above code, calling Car.new/1 spawns the process and returns a process ID, or "pid." It then sends a message to this pid using send/2. send/2 is in essence the same as calling a method in Ruby. The originator of the term object orientation, Alan Kay, seems disappointed that message passing has been displaced by method invocation. The major difference between message passing and method invocation is that message passing is asynchronous - more on that later.

That's our basic object. We have encapsulated state and provided behavior that changes the state. The Ruby version hides state in an instance variable and the Elixir version makes state explicit as a recursive function argument. The Ruby version calls methods, the Elixir version passes messages.

Inheritance

Beyond state and behavior, inheritance is another core tenet of object oriented programming. Inheritance allows us to extend types (classes) with new state and behavior.

Inheritance is a first class citizen in Ruby, making it easy to categorize state and behavior into subtypes. The following code should be palpable to all Rubyists. This code creates a new Truck type as a subtype of Car and adds an #offroad method only available to trucks.

# ruby
class Truck < Car
def offroad
puts "Going offroad."
end
end

Since we inherited from the Car class, we can call both the #drive and the #offroad methods on an instance of the Truck class.

That's the Ruby version. Elixir doesn't have classes. Inheritance in Elixir is not a first class citizen. It's going to require more setup and ceremony to accomplish.

First, how do we represent types and subtypes without classes? The observant reader would have noticed that upon defining the Car module in Elixir, one of the default values was a field named type with a value of "Car." In Elixir, classes and types can be represented as plain data, as binaries (strings). The concept of using data to represent types, and not concrete classes like with Ruby, is pervasive in functional programming - take for example records and tagged tuples.

To model inherited types in Elixir we'll use data to represent the Car type and the Truck subtype. To mimic the inheritance of behavior (methods) that a subtype derives from a parent type, we'll maintain an instance of Car that we delegate message to.

We need to keep our parent process around so that we can delegate messages when the subtype doesn't directly respond. Then, we call our run/1 function.

# elixir
Map.merge(%{parent: parent}, typed_state) |> run

The run/1 function in the Truck module should look familiar. We've added a new :offroad message that we respond to. When the Truck process receives a message it doesn't understand, it forwards it on to the parent Car process.

You can see that the Truck type has inherited all of the behavior of the Car type.

Polymorphism

Polymorphism is one of object orientation's strongest qualities. It is to programming what interchangeable parts is to manufacturing. It allows us to substitute subtypes for their parent type wherever the parent type is used. In addition in Ruby and Elixir, it allows us to substitute any type for another type as long as it responds to the right method or message.

Just like inheritance, polymorphism adheres to the Liskov substitution principle, a well established characteristic of good object oriented design and part of the SOLID design principles.

First, polymorphism in Ruby. We'll use the Car and Truck instances to show that they are interchangeable with regards to the #drive method. We'll randomly select either instance and call #drive.

The Array#sample method will return either a Car or a Truck instance, but since these are duck typed objects we can successfully call #drive on either. If we had another class that didn't inherit from Car but also contained a #drive method, we could substitute an instance of that class here as well. Polymorphism at its finest.

It's equally as easy in Elixir. Instead of calling methods, we'll pass messages. The only major difference between the Ruby and Elixir version is how we select the random object or process. The rest is virtually identical.

Polymorphism is an inherent part of Elixir, though it's seldom thought of this way. An Elixir process will gladly receive any message you pass it, regardless of whether it can do something with that message. There are no restrictions on which messages can be passed. A phantom message will simply sit in the process's mailbox, and it behooves me to mention that unhandled messages can cause memory leaks.

Asynchrony

For most intents and purposes, we've built the major components of an object oriented system in a functional language. It has some flaws and doesn't use the most sophisticated Elixir tools, but it showcases that it's possible to represent these patterns in Elixir.

There's one not-so-subtle nuance hidden in these code examples that would rear its head immediately upon actual implementation. Calling methods in Ruby is synchronous and passing messages in Elixir is asynchronous. In other words, calling a Ruby method will pause the program, execute the body of that method, and return the result of that method to the caller. It's a blocking, synchronous activity. Passing a message in Elixir is a non-blocking, asynchronous activity. Elixir will send a process a message and immediately return without waiting for the message to be received.

This can make trivial things in Ruby more cumbersome in Elixir. Take for example simply trying to return a value from a passed message. In Ruby this is straightforward.

# ruby
class Car
def color
"Red"
end
end
Car.new.color #=> Red

We can do the same thing in Elixir when we're not talking about message passing. Below, we're calling a function that has a return value and everything works as expected.

But once we start working with processes, this becomes more challenging. Here is an intuitive but broken piece of Elixir code.

# elixir
defmodule Car do
def new do
spawn_link(&run/0)
end
def run do
receive do
:color -> "Red"
end
end
end
car = Car.new
send(car, :color) #=> :color

Did you expect that sending the car process a :color message would return the value "Red"? Instead, the return value is :color. send/2 returns the message that was sent to a process, not the value that was returned once the message has been handled.

Message passing in Elixir is asynchronous, but if we want to model the synchronous behavior of Ruby's method invocation we'll have to get a little creative.

Since receive blocks the process and waits for a message, we can use that in the context of our caller. So, whoever calls :color would need to block and wait for a response in order to continue the program, just like Ruby.

Unlike Ruby, there's a bit more ceremony in getting this to work. We'll need to send the caller's pid into the callee. The callee will then send a message back to the caller with the final return value.

# elixir
defmodule Car do
def new do
spawn_link(&run/0)
end
def run do
receive do
{:color, caller} ->
send(caller, {:color, "Red"})
end
end
end
car = Car.new
send(car, {:color, self})
receive do
{:color, response} => response
end #=> Red

In the above code, we are passing the caller's pid into the callee, which can be accessed by calling self/0. The caller then waits for a message from the callee containing the response. In the caller, the response is pattern matched to extract the value. The return value from the caller's receive block is the final response of "Red".

That's a lot of ceremony. Luckily, Elixir has nice abstractions to avoid the litany. Here, we'll look at Agents. Using Agents, we can treat our code synchronously again and eliminate the low-level send and receive functions.

Elixir has a variety of tools that help keep our code clean while programming synchronously. One such tool is GenServer.call/3. GenServers are a very useful abstraction around processes that allow us to implement state and behavior in a simplified form, much like Ruby.

As a final thought around asynchrony, I'd like to mention two things.

Message passing in Elixir is slower than method invocation in Ruby. This is due to the delay between when the message is sent and when the receiving process handles it.

Message passing in Elixir is the primitive concurrency construct. It's the actor model of concurrency. This is not an option in Ruby unless you're using a library like Celluloid. Concurrency in Ruby is usually threaded. The actor model is an abstraction on threads, baked into Elixir, that provides a level of concurrency not attainable in Ruby.

Wrapping Up

We've blended object orientation and functional programming throughout the course of this article. Whether you prefer the Ruby version or the Elixir version, they both have their place.

Object orientation in Ruby is simple, elegant, and makes me happy. It doesn't provide the concurrency controls that Elixir offers, but the programming model is pleasant to use. On the other hand, Elixir allows us to model a system in an object oriented fashion while leveraging more powerful concurrency controls.

Object orientation in Elixir may or may not be a viable approach. I don't have enough data yet to draw conclusions. It is worth mentioning again that the functional community uses different patterns. The creator of Erlang, Joe Armstrong, has lamented over OOP due to the blending of state and behavior, though I find this mixture inevitable with processes. So while it may not be commonplace to use functional languages in an objected oriented style, it's certainly possible and may be more graceful when modeling some domains.

There's an antipattern I see in a lot of codebases. This antipattern obstructs our vision and makes code more difficult to reason about. It's everywhere, and its name is attr_reader.

In order to understand why attr_reader can be an antipattern, we need to appreciate that methods, inputs and instance variables are not the same thing. Functional programming has aided in formalizing this antipattern for me, but I've felt it all along.

In the following code, what are first_name and last_name?

def full_name
first_name + ' ' + last_name
end

You can speculate, but you can't be 100% confident. They're definitely methods, but looking strictly at this code, it's unclear where first_name and last_name are defined. Likely, there are attr_readers somewhere, and they are merely masked for their true values, which are instance variables:

def full_name
@first_name + ' ' + @last_name
end

It's difficult to know for sure where the values behind first_name and last_name come from. They could be caught by method_missing, in which case there's probably more complexity to fetching these values than appears on the surface. They could be defined on some entirely different object or contain some bizarre side effects in whatever code serves them. Only once the programmer reading full_name has investigated the source of first_name and last_name can they have confidence in knowing what this code does.

So how can we add clarity? Two forms: inputs and instance variables.

We've already seen the instance variable form of circumventing this antipattern:

def full_name
@first_name + ' ' + @last_name
end

This code disambiguates the source of first_name and last_name. They're instance variables; they come from within this class. There's no arguing, no guessing, they're definitely variables encapsulated within the same object that full_name is defined.

We can now look at full_name and reason about its proximity to first_name and last_name. This code improves the spatial locality of first_name and last_name by reducing the steps we must take to understand full_name.

Improving spatial locality reduces the cognitive complexity induced by this method. This is beneficial, especially when picking up new codebases, as it allows the reader to be confident in the code they see. It curtails the number of assumptions they must make about what's going on.

But we can do better than instance variables.

Instance variables improve spatial locality as they inform us that the source of some data is within the context of that class. But it doesn't tell us specifically where the instance variables are defined; we still need to trudge through code within that file. We have a tool that even further improves spatial locality: inputs.

In the following code, we defined first_name and last_name as parameters to the full_name method:

def full_name(first_name, last_name)
first_name + ' ' + last_name
end

Now we can look at this code, in isolation, and reason about the source of all data. We know exactly where first_name and last_name come from.

This is a functional style of programming, and I won't claim that it's always better. It might feel superfluous to have to pass first_name and last_name into this method in every place the full name was needed. It would make our code more verbose and less expressive. Which would you rather see throughout your codebase: person.full_name or person.full_name(person.first_name, person.last_name)?

But there is still yet another benefit to passing in arguments. Object oriented programmers might sigh when I say this, but this full_name method is now functionally pure. It is functionally pure because at any point, if we call this method with the same first_name and last_name arguments, we will get the exact same return value every time.

Functionally pure methods are easier to test as they require less setup. Whereas with instance variables, the object state must be correct before evoking the full_name method.

I recognize that for some people, using attr_reader is a stylistic choice. They prefer to look at the @-less reference to data, so they use attr_reader everywhere. I personally don't find @ symbols offensive. Their presence indicates a lot to me.

I do believe that attr_reader can be an antipattern, unless the intention is to expose the state of an instance variable to the outside world. We can avoid this antipattern by revealing underlying instance variables, and carefully considering granularity of inputs. Doing so can reduce complexity and dispel the magic of unnecessary abstractions.

For years, I didn't understand protected methods. Not because I didn't care to, but because I couldn't see the practicality. I'd been writing what I thought was quality production software and never needed them. Not once. It also didn't help that most explanations of protected methods evoked flashbacks of my worst classes in college when I realized mid-semester I had no idea what was going on. I'm not sure if that was my fault or the professor's.

Definitions of protected usually go like this: "protected methods can be accessed by other classes in the same package as well as by subclasses of its class in a different package." Uh, what?

I picked a particularly obscure definition above, but it was the third hit on google for "ruby protected methods."

Let's get one thing out of the way early. I'm not saying you shouldn't use protected methods. I'm saying you shouldn't "use" them. As in, deliberately use them with foresight. That's why I put the word in quotes. There are perfectly valid use cases for protected methods, and I'll illuminate one, but this tool should be employed as a refactoring clarification and nothing else.

Let me show you what I mean.

Say we have a Student class. Each student has a first name, last name, and the ability to provide their full name. Because knowing strictly a first or a last name is potentially ambiguous, a student only knows how to answer by their full name, so the first and last name are private methods.

Along come the professors and they want to check attendance. They plan to call attendance in alphabetical order by the students' last names. They've asked our company, Good Enough Software LLC, to find a way to sort the students by last name. We promptly tell the professors that we only have access to the students' full names. The professors quickly retort, "don't care, make it good enough."

We got this.

Since we can't call the private method #last_name, sorting by last name is a tricky task. We can't just write the following, where a classroom can sort its students:

Protected methods can't help us here. This code will not work unless the #last_name method is made public. We don't want to introduce ambiguity, so we can't make #last_name public.

We need to refactor, eventually to protected methods.

This is why I say don't "use" protected methods. Using protected methods during the first iteration of a class is like grabbing your sledgehammer because you heard there would be nails. You show up only to realize the thing you'll be hammering is your grandma's antique birdbox. Inappropriate use of protected methods dilutes the intention of the object's API, damaging its comprehensibility. When reading code that utilizes protected methods, I want to be able to assume there is an explicit need for it; public and private would not suffice. Unfortunately, this is seldom the case.

We should never write new code with protected methods. There's simply not a strong case for it.

But they are helpful here. If we instead compare the two student objects directly with the spaceship operator (<=>), then we can let the student objects compare themselves using #last_name. Since private methods are accessible by the object that owns them, maybe that will work? Let's try.

We want the Classroom class to look like the following, comparing student objects instead of the last name of each student.

class Classroom
def initialize(students)
@students = students
end
def alphabetized_students
students.sort do |one, two|
one <=> two
end
end
end

The use of the #sort method with a block above is the default behavior, so we can update the the code to eliminate the block:

class Classroom
def initialize(students)
@students = students
end
def alphabetized_students
students.sort
end
end

We now introduce the spaceship operator on the Student class to compare last names of students.

This code still won't run. The implicit call to last_name works, but the explicit call to other.last_name is attempting to call a private method on the other student object. Only now can protected methods save our metaphorical bacon.

Let's update the Student class to make #last_name protected. This will allow our spaceship method to call other.last_name, because the other object is also a Student.

So this is why I say we shouldn't "use" protected methods as a general purpose tool. It's strictly a refactoring clarification for cases where we'd like to provide some utility without exposing additional API to the outside world. In our case, we'd like to compare two students without exposing the #last_name method publicly.