Elixir for Ruby Developers
Learn the essentials of the Elixir programming language from the point of view of a Ruby developer. Let the Elixir vs Ruby showdown begin!
- Name
- Matt
- @mplatts
1 year ago
Variables
In Ruby:
name = "John"
puts name # Output: John
In Elixir:
name = "John"
IO.puts name # Output: John
Lists
In Ruby:
fruits = ["apple", "banana", "orange"]
puts fruits[0] # Output: apple
In Elixir:
fruits = ["apple", "banana", "orange"]
IO.puts Enum.at(fruits, 0) # Output: apple
Functions
In Ruby:
def add_numbers(a, b)
return a + b
end
puts add_numbers(2, 3) # Output: 5
In Elixir:
defmodule Math do
def add_numbers(a, b) do
a + b
end
end
IO.puts Math.add_numbers(2, 3) # Output: 5
Both Ruby and Elixir support defining functions, but in Elixir we need to define a module to contain the function. We also don’t need to use the return
keyword in Elixir - the last expression in the function is automatically returned.
Maps
In Ruby:
person = { name: "John", age: 30 }
puts person[:name] # Output: John
In Elixir:
person = %{ name: "John", age: 30 }
IO.puts person[:name] # Output: John
Loops
In Ruby:
for i in 1..3 do
puts i
end
# Output: 1 2 3
In Elixir:
1..3 |> Enum.each(fn i -> IO.puts i end)
# Output: 1 2 3
Conditional statements
In Ruby:
age = 25
if age >= 18
puts "You are an adult"
else
puts "You are a minor"
end
# Output: You are an adult
In Elixir:
age = 25
if age >= 18 do
IO.puts "You are an adult"
else
IO.puts "You are a minor"
end
# Output: You are an adult
Immutability
Everything is immutable in Elixir. When we perform an operation on a data structure, we create a new copy of the structure with the modification applied, rather than modifying the original structure in place.
In Ruby:
array1 = [1, 2, 3]
array2 = array1.dup
array2.delete_at(1)
p array1
p array2
# Output: [1, 2, 3] [1, 3]
In Elixir:
list1 = [1, 2, 3]
list2 = List.delete_at(list1, 1)
IO.inspect list1
IO.inspect list2
# Output: [1, 2, 3] [1, 3]
This demonstrates how we can achieve immutability in Ruby by creating copies of data structures before modifying them, rather than modifying the original structures in place.
Piping from one function to another
In Ruby:
name = "John"
puts name.upcase.reverse
# Output: NHOJ
In Elixir:
name = "John"
|> String.upcase()
|> String.reverse()
|> IO.puts()
# Output: NHOJ
Modules
In Ruby:
module Math
def self.add_numbers(a, b)
return a + b
end
end
puts Math.add_numbers(2, 3)
# Output: 5
In Elixir:
defmodule Math do
def add_numbers(a, b) do
a + b
end
end
IO.puts Math.add_numbers(2, 3)
# Output: 5
Both Ruby and Elixir support modules, but in Elixir we can define functions inside modules without using the self
keyword. We can also use the do
keyword to define the body of the function.
Anonymous functions
In Ruby:
add_numbers = ->(a, b) { a + b }
puts add_numbers.call(2, 3)
# Output: 5
In Elixir:
add_numbers = fn(a, b) -> a + b end
IO.puts add_numbers.(2, 3)
# Output: 5
Pattern matching
In Ruby:
a, b = :ok, 42
puts "#{a}, #{b}"
# Output: ok, 42
head, *tail = [1, 2, 3]
puts "#{head}, #{tail.inspect}"
# Output: 1, [2, 3]
person = {name: "Alice", age: 30, job: "Engineer"}
name, age = person.values_at(:name, :age)
puts "#{name}, #{age}"
# Output: Alice, 30
In Elixir:
{a, b} = {:ok, 42}
IO.puts("#{a}, #{b}")
# Output: ok, 42
[head | tail] = [1, 2, 3]
IO.puts("#{head}, #{inspect(tail)}")
# Output: 1, [2, 3]
%{name: name, age: age} = %{name: "Alice", age: 30, job: "Engineer"}
IO.puts("#{name}, #{age}")
# Output: Alice, 30
Both Ruby and Elixir support destructuring, but in Elixir we can use pattern matching to extract values from a list or tuple.
Function pattern matching
We can also use pattern matching to define function clauses with different argument patterns.
In Ruby:
def add_numbers(a, b)
if a.is_a?(Integer) && b.is_a?(Integer)
return a + b
elsif a.is_a?(String) && b.is_a?(String)
return a + b
else
raise "Invalid argument types"
end
end
puts add_numbers(2, 3)
puts add_numbers("Hello", "world")
puts add_numbers(2, "Hello")
# Output: 5 Helloworld Invalid argument types
In Elixir:
defmodule Math do
def add_numbers(a, b) when is_integer(a) and is_integer(b) do
a + b
end
def add_numbers(a, b) when is_binary(a) and is_binary(b) do
a <> b
end
def add_numbers(_, _), do: raise "Invalid argument types"
end
IO.puts Math.add_numbers(2, 3)
IO.puts Math.add_numbers("Hello", "world")
IO.puts Math.add_numbers(2, "Hello")
# Output: 5 Helloworld Invalid argument types
Pattern matching in Elixir really has no limits:
defmodule Example do
def greet({:person, name, age}) do
IO.puts("Hello, #{name}! You are #{age} years old.")
end
def greet({:animal, type, name}) do
IO.puts("Hello, #{name} the #{type}!")
end
end
Example.greet({:person, "Alice", 30})
# Output: Hello, Alice! You are 30 years old.
Example.greet({:animal, "cat", "Whiskers"})
# Output: Hello, Whiskers the cat!
Ranges
In Ruby:
numbers = (1..5).to_a
puts numbers[0]
puts numbers[-1]
# Output: 1 5
In Elixir:
numbers = 1..5 |> Enum.to_list()
IO.puts Enum.at(numbers, 0)
IO.puts Enum.at(numbers, -1)
# Output: 1 5
Keyword lists
In Ruby:
options = { color: "red", size: "medium" }
puts options[:color]
puts options[:size]
# Output: red medium
In Elixir:
options = [color: "red", size: "medium"]
IO.puts Keyword.get(options, :color)
IO.puts Keyword.get(options, :size)
# Output: red medium
Structs
In Ruby:
Person = Struct.new(:name, :age)
person = Person.new("John", 30)
puts person.name
puts person.age
# Output: John 30
In Elixir:
defmodule Person do
defstruct name: "", age: 0
end
person = %Person{name: "John", age: 30}
IO.puts person.name
IO.puts person.age
# Output: John 30
Guard clauses
In Ruby:
def add_numbers(a, b)
raise ArgumentError, "Invalid argument" if a < 0 || b < 0
return a + b
end
puts add_numbers(2, 3)
puts add_numbers(-2, 3)
# Output: 5 ArgumentError: Invalid argument
In Elixir:
def add_numbers(a, b) when a >= 0 and b >= 0 do
a + b
end
def add_numbers(_, _), do: raise "Invalid argument"
IO.puts add_numbers(2, 3)
IO.puts add_numbers(-2, 3)
# Output: 5 ** (RuntimeError) Invalid argument
Tuples
Tuples are intended as fixed-size containers for multiple elements. A tuple may contain elements of different types, which are stored contiguously in memory.
In Ruby:
person = ["John", 30]
puts person[0]
puts person[1]
# Output: John 30
In Elixir:
person = {"John", 30}
IO.puts elem(person, 0)
IO.puts elem(person, 1)
# Output: John 30
Substrings
In Ruby:
greeting = "Hello, world!"
puts greeting[0..4]
# Output: Hello
In Elixir:
greeting = "Hello, world!"
IO.puts String.slice(greeting, 0..4)
# Output: Hello
List comprehensions
In Ruby:
numbers = [1, 2, 3, 4, 5]
even_numbers = numbers.select { |n| n.even? }
puts even_numbers
# Output: [2, 4]
In Elixir:
numbers = [1, 2, 3, 4, 5]
even_numbers = for n <- numbers, rem(n, 2) == 0, do: n
IO.inspect even_numbers
# Output: [2, 4]
Both Ruby and Elixir support list comprehensions, but in Elixir we use the for
keyword to define the comprehension. We can use the <-
syntax to iterate over a list, and we can use a conditional expression to filter the values.
Binary pattern matching
In Ruby:
greeting = "Hello, world!"
if greeting =~ /Hello/
puts "Greeting starts with Hello"
else
puts "Greeting does not start with Hello"
end
# Output: Greeting starts with Hello
In Elixir:
greeting = "Hello, world!"
case String.match?(greeting, "Hello") do
true -> IO.puts "Greeting starts with Hello"
false -> IO.puts "Greeting does not start with Hello"
end
# Output: Greeting starts with Hello
Module attributes
In Ruby:
module Math
PI = 3.14
end
puts Math::PI
# Output: 3.14
In Elixir:
defmodule Math do
@pi 3.14
def pi, do: @pi
end
IO.puts Math.pi()
# Output: 3.14
Both Ruby and Elixir support module attributes, but in Elixir we use the @
symbol to define them. We can define a function that returns the value of the attribute.
try/catch blocks
In Ruby:
begin
raise "Error"
rescue => e
puts "Caught exception: #{e.message}"
end
# Output: Caught exception: Error
In Elixir:
try do
raise "Error"
catch
_ -> IO.puts "Caught exception"
end
# Output: Caught exception
The _
means ignore the exception variable. You could replace that with a variable name and print it out.
Private functions
In Ruby:
class Math
def self.add_numbers(a, b)
validate(a, b)
return a + b
end
private_class_method def self.validate(a, b)
raise "Invalid argument" if a < 0 || b < 0
end
end
puts Math.add_numbers(2, 3)
puts Math.add_numbers(-2, 3)
# Output: 5 Math::validate is private...
In Elixir:
defmodule Math do
def add_numbers(a, b) do
validate(a, b)
a + b
end
defp validate(a, b) do
if a < 0 or b < 0 do
raise "Invalid argument"
end
end
end
IO.puts Math.add_numbers(2, 3)
IO.puts Math.add_numbers(-2, 3)
# Output: 5 ** (RuntimeError) Invalid argument
Both Ruby and Elixir support private functions, but in Elixir we use the defp
keyword to define them. We can call a private function from within the module, but we can’t call it from outside the module.
Parallel processes
In Elixir:
defmodule Parallel do
def run_in_parallel(tasks) do
tasks
|> Enum.map(&Task.async(fn -> &1.() end))
|> Enum.map(&Task.await(&1))
end
end
task1 = fn -> IO.puts "Task 1 started"; :timer.sleep(1000); IO.puts "Task 1 finished" end
task2 = fn -> IO.puts "Task 2 started"; :timer.sleep(500); IO.puts "Task 2 finished" end
task3 = fn -> IO.puts "Task 3 started"; :timer.sleep(2000); IO.puts "Task 3 finished" end
Parallel.run_in_parallel([task1, task2, task3])
In Ruby:
require 'parallel'
task1 = lambda do
puts "Task 1 started"
sleep(1)
puts "Task 1 finished"
end
task2 = lambda do
puts "Task 2 started"
sleep(0.5)
puts "Task 2 finished"
end
task3 = lambda do
puts "Task 3 started"
sleep(2)
puts "Task 3 finished"
end
Parallel.each([task1, task2, task3], in_threads: 3) do |task|
task.call
end
Streams
Streams are lazy, composable enumerables.
In Elixir:
stream = Stream.iterate(1, &(&1 + 1))
|> Stream.filter(&rem(&1, 2) == 0)
|> Stream.map(&(&1 * &1))
Enum.take(stream, 5) |> IO.inspect
# Output: [4, 16, 36, 64, 100]
In this example, we use the Stream.iterate
function to create an infinite stream of numbers starting from 1. We then filter the stream to keep only the even numbers using the Stream.filter
function. Finally, we map each number to its square using the Stream.map
function.
We use the Enum.take
function to take the first five elements of the stream and print them to the console.
In Ruby:
stream = (1..Float::INFINITY)
.lazy
.select { |n| n % 2 == 0 }
.map { |n| n * n }
stream.take(5).to_a.each { |n| puts n }
# Output: 4 16 36 64 100
In this example, we use Ruby’s lazy
enumerator to create an infinite stream of numbers starting from 1. We then select only the even numbers using the select
method and map each number to its square using the map
method.
We use the take
method to take the first five elements of the stream and print them to the console.
Importing functions
In Elixir:
defmodule Math do
def add_numbers(a, b) do
a + b
end
end
defmodule Stats do
import Math, only: [add_numbers: 2]
def average(numbers) do
sum = Enum.reduce(numbers, 0, &add_numbers/2)
sum / length(numbers)
end
end
IO.puts Stats.average([1, 2, 3, 4, 5])
# Output: 3.0
In Ruby:
module Math
def self.add_numbers(a, b)
a + b
end
end
module Stats
extend Math
def self.average(numbers)
sum = numbers.reduce(0, &method(:add_numbers))
sum / numbers.length.to_f
end
end
puts Stats.average([1, 2, 3, 4, 5])
# Output: 3.0
Macro basics
Macros allow us to manipulate syntax trees and generate code dynamically.
In Elixir:
defmodule MyModule do
defmacro double(expression) do
quote do
unquote(expression) * 2
end
end
end
result = MyModule.double(3 + 4)
IO.puts result
# Output: 14
In this example, we define a module called MyModule
with a macro called double
. The macro takes an expression as an argument and doubles its value.
Inside the macro, we use the quote
macro to capture the expression as a syntax tree. We then use the unquote
macro to interpolate the expression into the syntax tree and multiply it by 2.
When we call MyModule.double(3 + 4)
, the macro expands to 7 * 2
, which evaluates to 14. We then print the result to the console.
In Ruby:
module MyModule
def self.double(expression)
eval("#{expression} * 2")
end
end
result = MyModule.double(3 + 4)
puts result
# Output: 14
In this example, we define a module called MyModule
with a method called double
. The method takes an expression as an argument and doubles its value.
Inside the method, we use string interpolation and the eval
method to evaluate the expression and multiply it by 2.
When we call MyModule.double(3 + 4)
, the expression is evaluated as 7 * 2
, which evaluates to 14. We then print the result to the console.
Processes
In Elixir:
defmodule MyModule do
def run() do
pid = spawn(fn -> IO.puts "Child process started"; :timer.sleep(1000); IO.puts "Child process finished" end)
IO.puts "Parent process continuing..."
Process.wait(pid)
IO.puts "Child process completed"
end
end
MyModule.run()
# Output: Parent process continuing... Child process started Child process finished Child process completed
In this example, we define a module called MyModule
with a function called run
. The function creates a new process using the spawn
function, which runs a block of code that prints a message, sleeps for one second, and then prints another message.
In the main process, we print a message indicating that the parent process is continuing. We then wait for the child process to complete using the Process.wait
function and print a message indicating that the child process has completed.
When we call MyModule.run()
, we should see the messages printed in the order shown in the output.
In Ruby:
def run()
child = fork do
puts "Child process started"
sleep(1)
puts "Child process finished"
end
puts "Parent process continuing..."
Process.wait(child)
puts "Child process completed"
end
run()
# Output: Parent process continuing... Child process started Child process finished Child process completed
In this example, we define a function called run
that creates a new process using the fork
method, which runs a block of code that prints a message, sleeps for one second, and then prints another message.
In the main process, we print a message indicating that the parent process is continuing. We then wait for the child process to complete using the Process.wait
method and print a message indicating that the child process has completed.
When we call run()
, we should see the messages printed in the order shown in the output.
We hope this gave you a good comparison of these two great languages.
The end.