DEV Community 👩‍💻👨‍💻

Christian Kreiling
Christian Kreiling

Posted on

Polymorphism: Elixir vs. Java

Introduction

This post seeks to enhance the understanding of Elixir protocols by comparing them to class hierarchies in Java. Though the two languages are much different, they share a common, powerful feature: Polymorphism.

When you first start learning Elixir, the first thing people say you have to do is wrap your head around Functional Programming (FP). This might mean kicking some old habits that have formed from using traditionally object-oriented languages such as Java and Python, and thinking slightly differently when you're writing code and designing software.

Though every Elixir dev will tell you how great FP is and why they love it, a lesser-known feature to beginner Elixir devs is its support for polymorphism through Protocols.

Polymorphism is a concept that is at the core of object-oriented programming, making it a feature that seems contrary to the core of the Elixir language. We'll see why it's useful, and compare them to familiar, foundational features in Java.

Who is this article for?

This article introduces the use of protocols in Elixir to achieve polymorphic functionality. If you've never heard of Elixir before, be sure to check out the Elixir website for more details on this exciting young language.

If you're at all familiar with Object Oriented Programming (OOP) in Java, this post will help to draw similarities between Java class hierarchies and Elixir protocols.

What is polymorphism?

Polymorphism is when our program dispatches to functions or methods based on the given data's type, or class.

For example, in Java we have the ability to define an interface, which only contains method definitions that outline some functionality. We then provide the implementations for an interface's method definitions when we define a class which implements that interface.

If a method's signature accepts an instance of an interface and calls a method on that instance, then at runtime, Java decides which method definition to dispatch to based on the underlying implementation class.

This allows us to decouple our code by not tying our implementation of one module, to the implementation of another module.

Java class hierarchies

Let's use a simple example here. Americans measure their height in inches, whereas the rest of the world uses centimetres. I'm sure some parts of the world use other units of measurement, but for the sake of simplicity we will stick with that assumption.

Let's define an interface for measuring Americans, or someone from the rest of the world, in either centimetres or inches:

public interface MeasuredPerson {
  double measureInInches();
  double measureInCentimetres();
}

Now we can implement this interface in a couple of classes which represent people from America and the rest of the world:

public class American implements MeasuredPerson {
  private String name;
  private double heightInInches;

  public American(String name, double heightInInches) {
    this.name = name;
    this.heightInInches = heightInInches;
  }

  public double measureInInches() { return heightInInches; }
  public double measureInCentimetres() { return heightInInches * 2.54; }
}

public class RestOfTheWorld implements MeasuredPerson {
  private String name;
  private double heightInCentimetres;

  public American(String name, double heightInCentimetres) {
    this.name = name;
    this.heightInCentimetres = heightInCentimetres;
  }

  public double measureInInches() { return heightInCentimetres * 0.39; }
  public double measureInCentimetres() { return heightInCentimetres; }
}

Now that we have a couple of classes we can use, we can write a method that accepts a MeasuredPerson and prints out their height in inches.

public class Main {
  public static void main(string[] args) {
    American american = new American("John", 72);
    RestOfTheWorld restOfTheWorld = new RestOfTheWorld("Jean", 178);

    printInches(american);
    printInches(restOfTheWorld);
  }

  private void printInches(MeasuredPerson measuredPerson) {
    System.out.println("The person's height is: " + measuredPerson.measureInInches());
  }
}

This code will print out the height in inches of both the American and the person from the rest of the world. The method printInches dispatches the call to measureInInches at runtime based on the underlying type of the given object!

Elixir protocols

Protocols in Elixir allow for us to define similar relationships in our programs. First, we define a protocol, which is a set of function definitions that outline some functionality. We then provide the implementations for a protocol's function definitions when we define an implementation module for an Elixir type.

Elixir types include List, Map, and Keyword, but they can also include custom structs that we define. The following is an example of two Person structs, similar to our Java classes which we defined above:

defmodule Person.American do
  defstruct [:name, :height_in_inches]
end

defmodule Person.RestOfTheWorld do
  defstruct [:name, :height_in_centimetres]
end

Americans measure their height in inches, and the rest of the world measures their height in centimetres. What if we want to measure a person, without worrying about whether they're from America, or somewhere else in the world?

The defprotocol keyword

Similar to how we defined an interface in Java, we can define a protocol for measuring the height of a person. The protocol expects a unit of measurement which the user wants to measure a given person with.

defprotocol Person.Height do
  @doc """
  Accepts the person to measure as the first argument, and accepts either
  `:inches` or `:centimetres` as the second argument to indicate 
  which unit of measurement to return.
  """
  def measure(person, unit_of_measurement)
end

Notice that we don't provide a do block for our measure/2 function. The next section will show how we define an implementation for our fresh protocol.

The defimpl keyword

The following modules use the defimpl keyword to provide an implementation for our custom person structs:

defimpl Person.Height, for: Person.American do
  def measure(%Person.American{height_in_inches: height_in_inches}, :inches) do
    height_in_inches
  end

  def measure(%Person.American{height_in_inches: height_in_inches}, :centimetres) do
    height_in_inches * 2.54
  end
end

defimpl Person.Height, for: Person.RestOfTheWorld do
  def measure(%Person.RestOfTheWorld{height_in_centimetres: height_in_centimetres}, :inches) do
    height_in_centimetres * 0.39
  end

  def measure(%Person.RestOfTheWorld{height_in_centimetres: height_in_centimetres}, :centimetres) do
    height_in_centimetres
  end
end

defimpl takes a protocol module (defined using defprotocol), and the module mapping to the type that we want to provide an implementation for. The first argument to our measure/2 functions defined in our implementation modules is therefore a struct of that type.

There is a key difference from our Java implementation: instead of implementing the behavior inside the Person.American and Person.RestOfTheWorld modules, we implement the behavior in a separate module. Since structs themselves can't have behavior (i.e. you can't call american.measure(:centimetres)), the struct must be passed to the function instead; see below for how we do this.

Tying it all together

Now that we've defined a couple of implementation modules for our Person.American and Person.RestOfTheWorld structs, we can call the Person.Height.measure/2 function to measure the given person in our preferred unit of measurement!

%Person.American{name: "John", height_in_inches: 72}
|> Person.Height.measure(:centimetres)
|> IO.inspect()
# 182.88

%Person.RestOfTheWorld{name: "Jean", height_in_centimetres: 178}
|> Person.Height.measure(:inches)
|> IO.inspect()
# 70.0787

The power of protocols

We've used a simple example here to show how we can create our own protocols. But what's the big deal?

Using protocols is powerful because we can define a single interface with well-defined behavior once, and then implement that behavior elsewhere based on our custom struct.

A big advantage which Elixir protocols have over Java class hierarchies is that we can supply implementations of our own protocols for structs from external libraries. In Java, you'd have to edit the source code of other libraries to implement your custom interface. But in Elixir, all we have to do is provide an implementation for a type (struct), regardless of what library/framework provides that type!

A familiar example

In Elixir, we use the Enum module for traversing lists and maps. Anything we pass as the first argument to the functions in the Enum module must have an implementation for the Enumerable protocol! To read more about this check out the Enumerable docs.

Resources

I'd like to provide some additional resources for learning about Elixir Protocols, so you can continue to explore this OOP concept in a traditionally functional language.

  • Elixir protocols: the introduction to Elixir protocols on the official Elixir lang website.
  • Elixir Protocol docs: if you're looking for a deeper dive into how protocols work, this is a good place to start. The Elixir docs are written so well that usually I don't look elsewhere.
  • Programming Elixir >= 1.6: this book is one of the best tools in my arsenal. I couldn't recommend a better book for beginners and experts alike. I use it as reference all of the time.

I wrote this post for those of us that are familiar with the power of OOP, and to showcase an awesome feature in Elixir that feels right at home for anyone familiar with OOP in Java. Thanks for reading!

Top comments (1)

Collapse
 
maartz profile image
Maartz

Reminds me of Go ! Thanks for the article.

DEV has this feature:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. 🛠