José Valim
Former Rails Core Team member.
He was trying to make Rails really thread
safe but... ended up creating a new
programming language (Elixir). Oops!
PerformanceProductivity
Chris McCord
Author of render_sync a Ruby gem to
have real-time partials in Rails (before
ActionCable).
It got complicated and... he ended up
creating a new web framework
(Phoenix). Oops!
Elixir is a dynamic, functional language
designed for building scalable and
maintainable applications.
Elixir leverages the Erlang VM, known for
running low-latency, distributed and
fault-tolerant systems.
# /web/router.ex
defmodule TwitterDemo.Router do
use TwitterDemo.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", TwitterDemo do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
get "/timeline", PageController, :timeline
end
# Other scopes may use custom stacks.
# scope "/api", TwitterDemo do
# pipe_through :api
# end
end
# /config/routes.rb
Rails.application.routes.draw do
root 'page#index'
get '/timeline' => 'page#timeline'
end
# /web/router.ex
defmodule TwitterDemo.Router do
use TwitterDemo.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", TwitterDemo do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
get "/timeline", PageController, :timeline
end
# Other scopes may use custom stacks.
# scope "/api", TwitterDemo do
# pipe_through :api
# end
end
# /config/routes.rb
Rails.application.routes.draw do
root 'page#index'
get '/timeline' => 'page#timeline'
end
Plug
It’s an Elixir library that tries to solve the same problem than
Rack does for Ruby.
A plug is a function or module which always receives and returns
a connection, doing some data transformations in the middle.
When we compose multiple plugs we form a pipeline.
# /web/router.ex
defmodule TwitterDemo.Router do
use TwitterDemo.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", TwitterDemo do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
get "/timeline", PageController, :timeline
end
# Other scopes may use custom stacks.
# scope "/api", TwitterDemo do
# pipe_through :api
# end
end
$ rails g controller Page index timeline
# /app/controllers/page_controller.rb
class PageController < ApplicationController
def index
end
def timeline
end
end
# /web/controllers/page_controller.ex
defmodule TwitterDemo.PageController do
use TwitterDemo.Web, :controller
def index(conn, _params) do
render conn, "index.html"
end
def timeline(conn, params) do
conn
|> assign(:nickname, params["nickname"])
|> render("timeline.html")
end
end
# /app/controllers/page_controller.rb
class PageController < ApplicationController
def index
end
def timeline
end
end
# /web/controllers/page_controller.ex
defmodule TwitterDemo.PageController do
use TwitterDemo.Web, :controller
def index(conn, _params) do
render conn, "index.html"
end
def timeline(conn, params) do
conn
|> assign(:nickname, params["nickname"])
|> render("timeline.html")
end
end
Typical code in OOP / imperative programming:
people = DB.find_customers
orders = Orders.for_customers(people)
tax = sales_tax(orders, 2013)
filing = prepare_filing(tax)
We could rewrite it as...
filing = prepare_filing(
sales_tax(Orders.for_customers(
DB.find_customers), 2013))
Pipe Operator |>
Pipe Operator |>
With Elixir pipe operator we can do just
filing = DB.find_customers
|> Orders.for_customers
|> sales_tax(2013)
|> prepare_filing
“|>” passes the result from the left expression as
the first argument to the right expression. Kinda
like the Unix pipe “|”. It’s just useful syntax sugar.
# /web/models/message.ex
defmodule TwitterDemo.Message do
use TwitterDemo.Web, :model
@derive {Poison.Encoder, only: [:author, :content, :inserted_at]}
schema "messages" do
field :author, :string
field :content, :string
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params %{}) do
struct
|> cast(params, [:author, :content])
|> validate_required([:author, :content])
end
end
# /app/models/message.rb
class Message < ApplicationRecord
validates_presence_of :author, :content
end
# /web/models/message.ex
defmodule TwitterDemo.Message do
use TwitterDemo.Web, :model
@derive {Poison.Encoder, only: [:author, :content, :inserted_at]}
schema "messages" do
field :author, :string
field :content, :string
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params %{}) do
struct
|> cast(params, [:author, :content])
|> validate_required([:author, :content])
end
end
# /app/models/message.rb
class Message < ApplicationRecord
validates_presence_of :author, :content
end
Ecto
You could think about Ecto as “the ActiveRecord of Elixir”.
But better don’t. It’s not even an ORM (in its purest definition).
It’s a database wrapper and it’s main target it’s PostgreSQL.
Other database are supported too.
Main concepts behind Ecto are:
Schemas: each Model defines a struct with its schema.
Changesets: define a pipeline of transformations (casting, validation &
filtering) over our data before it hits the database.
$ rails g channel Timeline new_msg $ mix phoenix.gen.channel Timeline
# /app/channels/timeline_channel.rb
# Be sure to restart your server when you modify this file.
Action Cable runs in a loop that does not support auto
reloading.
class TimelineChannel < ApplicationCable::Channel
def subscribed
@nickname = params[:nickname]
stream_from "timeline"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def new_msg(payload)
# Careful with creating the record here
# http://www.akitaonrails.com/2015/12/28/fixing-dhh-s-
rails-5-chat-demo
message = Message.create!(content: payload['content'],
author: @nickname)
# DHH suggests doing this in a background job instead, I’m
not sure why?
ActionCable.server.broadcast 'timeline', message: message
end
end
$ mix phoenix.gen.channel Timeline
# /web/channels/user_socket.ex
defmodule TwitterDemo.UserSocket do
use Phoenix.Socket
## Channels
channel "timeline:lobby", TwitterDemo.TimelineChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
# transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# ...
def connect(params, socket) do
{:ok, assign(socket, :nickname, params["nickname"])}
end
# ....
def id(_socket), do: nil
end
# /app/channels/timeline_channel.rb
# Be sure to restart your server when you modify this file.
Action Cable runs in a loop that does not support auto
reloading.
class TimelineChannel < ApplicationCable::Channel
def subscribed
@nickname = params[:nickname]
stream_from "timeline"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def new_msg(payload)
# Careful with creating the record here
# http://www.akitaonrails.com/2015/12/28/fixing-dhh-s-
rails-5-chat-demo
message = Message.create!(content: payload['content'],
author: @nickname)
# DHH suggests doing this in a background job instead
ActionCable.server.broadcast 'timeline', message: message
end
end
# /web/channels/timeline_channel.ex
defmodule TwitterDemo.TimelineChannel do
use TwitterDemo.Web, :channel
alias TwitterDemo.Message
def join("timeline:lobby", payload, socket) do
# Add authorization logic here as required.
{:ok, socket}
end
def handle_in("new_msg", %{"content" => content,}, socket) do
changeset = Message.changeset(%Message{}, %{
content: content,
author: socket.assigns.nickname
})
case Repo.insert(changeset) do
{:ok, message} ->
broadcast! socket, "new_msg", %{message: message}
{:noreply, socket}
{:error, _changeset} ->
{:reply, {:error, %{error: "Error saving the message"}},
socket}
end
end
def handle_out("new_msg", payload, socket) do
push socket, "new_msg", payload
{:noreply, socket}
end
end
# /app/channels/timeline_channel.rb
# Be sure to restart your server when you modify this file.
Action Cable runs in a loop that does not support auto
reloading.
class TimelineChannel < ApplicationCable::Channel
def subscribed
@nickname = params[:nickname]
stream_from "timeline"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def new_msg(payload)
# Careful with creating the record here
# http://www.akitaonrails.com/2015/12/28/fixing-dhh-s-
rails-5-chat-demo
message = Message.create!(content: payload['content'],
author: @nickname)
# DHH suggests doing this in a background job instead
ActionCable.server.broadcast 'timeline', message: message
end
end
# /web/channels/timeline_channel.ex
defmodule TwitterDemo.TimelineChannel do
use TwitterDemo.Web, :channel
alias TwitterDemo.Message
def join("timeline:lobby", payload, socket) do
# Add authorization logic here as required.
{:ok, socket}
end
def handle_in("new_msg", %{"content" => content,}, socket) do
changeset = Message.changeset(%Message{}, %{
content: content,
author: socket.assigns.nickname
})
case Repo.insert(changeset) do
{:ok, message} ->
broadcast! socket, "new_msg", %{message: message}
{:noreply, socket}
{:error, _changeset} ->
{:reply, {:error, %{error: "Error saving the message"}},
socket}
end
end
def handle_out("new_msg", payload, socket) do
push socket, "new_msg", payload
{:noreply, socket}
end
end
# /app/channels/timeline_channel.rb
# Be sure to restart your server when you modify this file.
Action Cable runs in a loop that does not support auto
reloading.
class TimelineChannel < ApplicationCable::Channel
def subscribed
@nickname = params[:nickname]
stream_from "timeline"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def new_msg(payload)
# Careful with creating the record here
# http://www.akitaonrails.com/2015/12/28/fixing-dhh-s-
rails-5-chat-demo
message = Message.create!(content: payload['content'],
author: @nickname)
# DHH suggests doing this in a background job instead
ActionCable.server.broadcast 'timeline', message: message
end
end
Pattern Matching
In Elixir: a = 1 does not mean we are assigning 1 to the variable a.
Instead of assigning a variable, in Elixir we talk about binding a variable .
The equal signs means we are asserting that the left hand side (LHS) is
equal to the right one (RHS). It’s like basic algebra.
iex> a = 1
1
iex> 1 = a
1
iex> [1, a, 3] = [1, 2, 3]
[1, 2, 3]
iex> a
2
Pattern Matching
Function signatures use pattern matching.
Therefore we can have more than one signature.
defmodule Factorial do
def of(0), do: 1
def of(x), do: x * of(x-1)
end
look mum! programming without if - else
# /web/channels/timeline_channel.ex
defmodule TwitterDemo.TimelineChannel do
use TwitterDemo.Web, :channel
alias TwitterDemo.Message
def join("timeline:lobby", payload, socket) do
# Add authorization logic here as required.
{:ok, socket}
end
def handle_in("new_msg", %{"content" => content,}, socket) do
changeset = Message.changeset(%Message{}, %{
content: content,
author: socket.assigns.nickname
})
case Repo.insert(changeset) do
{:ok, message} ->
broadcast! socket, "new_msg", %{message: message}
{:noreply, socket}
{:error, _changeset} ->
{:reply, {:error, %{error: "Error saving the message"}},
socket}
end
end
def handle_out("new_msg", payload, socket) do
push socket, "new_msg", payload
{:noreply, socket}
end
end
# /app/channels/timeline_channel.rb
# Be sure to restart your server when you modify this file.
Action Cable runs in a loop that does not support auto
reloading.
class TimelineChannel < ApplicationCable::Channel
def subscribed
@nickname = params[:nickname]
stream_from "timeline"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def new_msg(payload)
# Careful with creating the record here
# http://www.akitaonrails.com/2015/12/28/fixing-dhh-s-
rails-5-chat-demo
message = Message.create!(content: payload['content'],
author: @nickname)
# DHH suggests doing this in a background job instead
ActionCable.server.broadcast 'timeline', message: message
end
end
Jose, Don’t forget to
mention OTP!
1. Send history of messages when connecting to
channel.
2. Add Presence module (to display who is online).
3. Create a startup with this, become a unicorn
and profit!
* Only 1. & 2. are solved here
https://github.com/diacode/talkex/tree/feature/message-db-persistence
hint: it takes 10 minutes with phoenix v1.2
Next steps (for you)
• Watch every talk by José Valim & Chris McCord
Really, you won’t regret.
• Books:
Programming Elixir – Dave Thomas
Programming Phoenix – Chris McCord, Bruce Tate & José Valim.
• Elixir Getting Started Guide (really good!)
http://elixir-lang.org/getting-started/introduction.html
• Phoenix Guide (really good!)
http://www.phoenixframework.org/docs/overview
• Elixir Radar (newsletter)
http://plataformatec.com.br/elixir-radar
• Madrid |> Elixir MeetUp
http://www.meetup.com/es-ES/Madrid-Elixir/
THANK YOU
Questions?
Special thanks go to Diacode’s former team:
Victor Viruete, Ricardo García, Artur Chruszcz & Bruno Bayón
<3
[ now you can blink again ]