Posts tagged with actors

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.