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)