DEV Community

bakenator
bakenator

Posted on

Intro to OOP in Elixir

Who is This For?

Like many others I came to the Elixir language through the Phoenix framework and then became interested in writing Elixir outside of Phoenix. But with its immutability and lack of class methods, my biggest challenge was figuring out how to get anything done.

This post is meant to go over some of the big lessons I learned about how to transition my OOP programming knowledge into productive Elixir code. Most of these points are very simple, they just need to "click" in your thinking.

Functions and Data are separate

In most OOP languages you use instances of classes (objects) to do the work of our program. Each instance has a self contained state and then functions that can do work by referencing that state. For instance a simple Ruby class may look like this.

class Person
   def initialize(name)
      @cust_name = name
   end

   def name_backwards
      @cust_name.reverse
   end
end

person_1 = Person.new('Ted')
person_1.name_backwards
# outputs "deT"

Elixir functions can not have any references to this, @, or any other instance variables. So the way to do something similar in Elixir is by having a function take the data (instance) as the first parameter. Here is the Ruby code above rewritten in Elixir.

person = %{name: "Ted"}

defmodule Person.Functions do
  def name_backwards(person) do
    String.reverse(person.name)
  end
end

Person.Functions.name_backwards(person)

In the code above we can see that the data of a person instance is kept completely outside of the Person.Functions module. When we want to use a function on the person instance, we pass it into a function that takes a person instance as the first parameter.

In practice a module would never be named Person.Functions because by definition the module is not tied to a person instance. The name_backwards function could be used on any object with a name field.

Update by Replacement

One of the most common things to do in OOP is to use an instance of an object to track some state. For example in Ruby

class Person
   def initialize(age)
      @person_age = age
   end

   def add_year
      @person_age = @person_age + 1
   end

   def get_age
      @person_age
   end
end

person_1 = Person.new(30)
person_1.add_year
person_1.add_year
person_1.get_age
# outputs 32

In Elixir all objects are immutable, so how on earth can we update a field of this person instance? It can be done by making a new person instance and replacing the old one.

person = %{age: 30}

defmodule Person.Functions do
  def add_year(person) do
    new_age = person.age + 1
    %{age: new_age}
  end

  def get_age(person) do
    person.age
  end
end

person = Person.Functions.add_year(person)
person = Person.Functions.add_year(person)
Person.Functions.get_age(person)

So instead of changing the value of the field within the instance, we return a new instance in the line %{age: new_age}.

Global Objects are stored in Processes

I am not advocating for the use of global state, however I do think this idea is fundamentally important to learning the basics of Elixir. Also this may be a contrived example, but I think it will show the challenge of translating OOP to Elixir.

I added a ticket class which dispenses tickets. It keeps track as each ticket goes out so that each person gets a unique ticket. Fairly simple in Ruby since we can use the static field @@ticket_count to keep track of the tickets.

class Tickets
  @@ticket_count = 1
  def self.get_ticket
    next_ticket = @@ticket_count
    @@ticket_count = @@ticket_count + 1
    next_ticket
  end
end

class Person
   def initialize()
      @ticket_num = -1
   end

   def get_ticket
    @ticket_num = Tickets.get_ticket
    @ticket_num
   end
end

person_1 = Person.new()
puts person_1.get_ticket
#outputs 1
person_2 = Person.new()
puts person_2.get_ticket
#outputs 2

So how would we keep track of the ticket counts in Elixir? By using what is called a GenServer.

defmodule Tickets do
  use GenServer

  def start_link(start_ticket_num) do
    GenServer.start_link(Tickets, start_ticket_num, name: Tickets)
  end

  def get_ticket() do
    GenServer.call Tickets, :get_ticket
  end

  def handle_call(:get_ticket, _from, curr_ticket_num) do
    {:reply, curr_ticket_num, curr_ticket_num + 1}
  end
end

defmodule Person.Functions do
  def get_ticket(person) do
    next_ticket = Tickets.get_ticket()
    %{ticket_num: next_ticket}
  end
end

Tickets.start_link(1)
person_1 = %{ticket_num: -1}
IO.inspect Person.Functions.get_ticket(person_1)
#output %{ticket_num: 1}
person_2 = %{ticket_num: -1}
IO.inspect Person.Functions.get_ticket(person_2)
# %{ticket_num: 2}

Now this is getting more advanced, but it is the real beauty of Elixir. In the start_link(1) function, what we did was start another process with an instance of a GenServer named Tickets with an initial value of 1.

GenServers can do a lot of things, but here we are using it to store the current ticket number. Each time we call get_ticket the code flows through to the response {:reply, curr_ticket_num, curr_ticket_num + 1} which is an order to the GenServer basically formatted as {command_to_genserver, response_value, new_state_value}. Now any Elixir module can get a ticket just by calling the Tickets.get_ticket function.

In most OOP languages, starting another process is a very high level task. In fact a dev can go for years more or less only working in the main process. In Elixir however working with other processes is a fundamental skill.

If you want to learn more about GenServers I would highly recommend writing your own! Here is a tutorial

Wrap Up

These were the first few things that came to my mind when thinking back about the challenges of learning Elixir. Hope one of them was helpful for you.

Coding in Elixir is Fun!

Top comments (0)