Skip to content

Instantly share code, notes, and snippets.

@Gaolz
Forked from tute/elixir-for-rubyists.md
Created August 20, 2016 10:47
Show Gist options
  • Save Gaolz/439a786f169524a596e204d49b9560ec to your computer and use it in GitHub Desktop.
Save Gaolz/439a786f169524a596e204d49b9560ec to your computer and use it in GitHub Desktop.

A top-down look into Elixir (for Rubyists)

Phoenix (as "Rails for Elixir")

Rails and Phoenix can get suspiciously similar

  • Both are MVC
  • Both provide a default directory structure
  • Both provide a default stack with relational database
  • Both provide a default testing setup
  • Both promote security by default
  • Both focus on productivity, client to server side

A code sample:

controller.rb

before_action :find_user

def show do
  @post = @user.posts.find(params[:id])
end

def find_user
  @user = User.find(params[:user_id])
end

controller.ex

plug :find_user

def show(conn, %{"id" => id}) do
  post = conn.assigns.user |> assoc(:posts) |> Repo.get(id)
  render conn, "show.html", post: post
end

defp find_user(conn, _) do
  assign(conn, :user, Repo.get(User, conn.params["user_id"]))
end

Phoenix (as Phoenix)

  • Convention over configuration. But not so much.
  • Rails controller state: params hash, request object, any instance variables
  • Phoenix controller state: conn (comparable to rack's env and response) passed as parameter
  • Phoenix controller actions are... functions! Easy to unit test

Live Reload

  • When you save any file in your application (whether it's SASS, Elixir or ES2015 JavaScript), appropriate files are recompiled and the browser is refreshed in a blink.
  • Brunch for asset pipeline
  • A feature I missed from Ember.

Elixir (as "Ruby for the Erlang VM")

  • Elixir's syntax feels familiar to rubyists
  • Both languages are high-level, readable, and fun

Feature Mapping

Ruby Elixir
irb iex
rake tasks mix (built in)
bundler dependencies mix (built in)
binding.pry IEx.pry (built in)
Polymorphism Protocols
Lazy Enumerables Streams
Metaprogramming Macros (used sparingly)
Rails Phoenix

The Macros documentation explicitly discourages its use, and then explains how they can extend the language.

Community

The fast growing supportive community is reminiscent of Ruby during the early days of Rails.

Elixir (as a language on top of Erlang)

  • Elixir runs on the Erlang virtual machine (BEAM)
  • Erlang was born three decades ago (at Ericsson in 1986)
  • Erlang has been Open Source for almost two decades (since 1998)
  • Erlang is now maintained by the Open Telecom Platform (OTP) unit at Ericsson

What makes it performant?

WhatsApp got two million TCP connection in a single box. What role does Erlang play in this?

Processes

Is Erlang uniquely performant? Do we lose anything to seize those benefits? TL;DR: We don't get mutability, and shared memory (almost always).

  • The Erlang VM runs as one OS process
  • It runs one OS thread per core by default (Elixir apps use all CPU cores)
  • Erlang processes are implemented by the VM and have no connection to OS processes or threads
  • An Erlang system running over one million (Erlang) processes may run one OS processes, and one thread per core
  • Cooperative threading: threads switch at controlled execution points instead of preemptively, and don’t have the CPU context switch penalty
  • Thread memory footprint starts very small compared to OS ones
  • Processes share no state with each other
  • Processes communicate through asynchronous messages
  • Erlang enforces immutable data, helping with thread safety and lock avoidance
  • GC is fast: Every variable is immutable, so a variable can never point to a value created after it
  • GC is fast: Values are copied between processes, so memory referenced in a process is isolated (almost always)
  • GC is fast: it runs per (small) process

Fault tolerance

  • Erlang’s architecture is share nothing: each node is independent and self-sufficient
  • No single point of failure
  • Allows self-healing and non-disruptive upgrades

Fault-tolerance in Erlang means “keep system running”:

  • it's ok to maybe drop a user’s phone call
  • it’s not ok to drop everyone’s phone call

Paraphrasing José Valim at The Changelog podcast:

Unlike the web, telecommunication companies can't call everyone and say there will be an outage between 6 and 6:30am. They have to keep the lights always on.

In terms of the CAP Theorem, you will typically get Availability, Partition Tolerance, and (only Eventual) Consistency.

Software runs concurrently by default

Horizontal vs. Vertical scaling

Features

  • Distributed
  • Fault-tolerant
  • Soft real-time (think telecommunications’ quality of service)
  • Highly available
  • Hot code swapping
  • Share nothing concurrency (the first popular Actor-based concurrency implementation)

Elixir (as Elixir)

While Elixir resembles Ruby, there are notable differences.

Erlang VM and OTP integration

  • Distributed, fault tolerant, concurrent, highly available foundations
  • A functional paradigm (if that's your thing)

No State

  • Functions are pure, with no side effects
  • Values are immutable

More explicit

Explicit > implicit

But not so much. It's a nice balance between:

“Go is cool because we spell out all da things!”

and

“Ruby is cool because a call to acts_as_concurrent makes everything ok”

It’s The Perfect Balance (disclaimer: opinion, not a fact)

Pipe Operator

Took this from Fabio Akita's blog post (and his talk in Goruco): http://www.akitaonrails.com/2016/02/18/elixir-pipe-operator-for-ruby-chainable-methods.

In Ruby we can write code like the following:

text.split(\n).map(&:strip).reject(&:empty?)

Each call returns an Enumerable object, and so we can chain Enumerable methods on each result. But what if we wanted to give names to each call?

(1..500).to_a.
  multiply_elements_by_three.
  keep_only_odd_elements.
  sum_elements

To make this work we'd need to monkey patch Ruby to define these methods on Array.

In a functional fashion, an Elixir version of that code is:

Enum.sum(Enum.filter(Enum.map(1..500, &(&1 * 3)), &Integer.is_odd/1)) # => 187_500

This could be more readable if we used local variables:

multiplied_by_three = Enum.map(1..500, &(&1 * 3))
only_odd_elements = Enum.filter(multiplied_by_three, &Integer.is_odd/1)
Enum.sum(only_odd_elements) # => 187_500

The pipe operator introduces the expression on the left-hand side as the first argument to the function call on the right-hand side. It allow us to refactor code like:

1..500
|> Enum.map(&(&1 * 3))
|> Enum.filter(&Integer.is_odd/1)
|> Enum.sum # => 187_500

Each step can be extracted into an intention revealing function:

1..500
|> multiply_elements_by_three
|> keep_odd_elements_only
|> sum_elements

The match operator: =

There will be no more assignments!

iex> {a, b, 42} = {:hello,world, 42}
{:hello,world, 42}
iex> a
:hello
iex> bworld

Pattern Matching

Example:

<%= for document <- documents do %>
  <li class="<%= active_class(@conn.assigns, document.id) %>">
    <%= document.title %>
  </li>
<% end% >

active_class implementation:

def active_class(%{document: %{id: id}}, id), do: activedef active_class(_, _), do: “”
  • Conventional control-flow statements not common
  • Many small functions, with guard clauses or pattern matching
  • Code results more declarative than imperative

When is Elixir better suited than Ruby?

I believe Elixir and Ruby are interchangeable for simple web applications with no high-traffic or very low response time demands.

For some applications, Elixir makes better technical sense:

High-traffic Systems

  • Elixir is faster by an order of magnitude
  • Erlang and Elxir, are great fit for multi-core hardware. Elixir’s programs are built in distributed manner even if they run on a single machine.

Distributed / Clustered Systems

  • Scale horizontally rather than vertically whenever possible
  • This can be done in any architecture with a proxy sitting in front of a “cluster”, and then sharing state gets tricky (Redis, single point of failure)

High-availability Systems

  • Fault tolerance
  • Code hot-swapping

Large Applications

  • Built-in tools to split the code base into chunks (Umbrella Projects)
  • Code is loosely coupled by design, enforced by the language itself

Links I consulted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment