Misc Blog

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
Twitter
@mplatts

11 months 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.

The end

More posts