Diese Präsentation wurde erfolgreich gemeldet.
Wir verwenden Ihre LinkedIn Profilangaben und Informationen zu Ihren Aktivitäten, um Anzeigen zu personalisieren und Ihnen relevantere Inhalte anzuzeigen. Sie können Ihre Anzeigeneinstellungen jederzeit ändern.
PHOENIX
FOR RAILS DEVS
Conferencia Rails
Madrid
15/10/2016
If you're having talk problems,
I feel bad for you, son.
I got 61 problems, but a slide
ain't one; hit me!
About me:
Javier Cuevas
@javier_dev
RUBY ON RAILS SHOP
WHO EMBRACED ELIXIR
AIRBNB FOR DOGS
“MAJESTIC” RAILS 3.2 MONOLITH
GUESTS
OF THE DAY
José Valim
Former Rails Core Team member.
He was trying to make Rails really thread
safe but... ended up creating a new
pr...
Chris McCord
Author of render_sync a Ruby gem to
have real-time partials in Rails (before
ActionCable).
It got complicated...
WHAT IS
ELIXIR?
Elixir is a dynamic, functional language
designed for building scalable and
maintainable applications.
Elixir leverages th...
WHAT IS
PHOENIX?
Phoenix is a productive web framework
for Elixir that does not compromise speed
and maintainability.
PHOENIX = PRODUCTIVITY + PERFORMANCE
PERFORMANCE
I don’t care about performance.
* that much
PRODUCTIVITY
SHORT
TERM
LONG
TERM
SHORT TERM
PRODUCTIVITY
The Phoenix Backpack
• Mix (generators, tasks, etc.)
• Erlang libraries + Hex.pm
• ES6 out of the box
• Live reload
• Nice...
Remember the “15 min blog” by DHH?
That was productivity!
Let’s try build the “15 min real time Twitter”
(or something clo...
https://github.com/javiercr/conferencia_ror_demo
LET’S
GET STARTED
rails new twitter_demo mix phoenix.new twitter_demo
!"" twitter_demo
#"" app
$   #"" assets
$   #"" channels
$   #"" controllers
$   #"" helpers
$   #"" jobs
$   #"" mailers
...
$ cd twitter_demo
$ bundle install
$ rake db:create
$ rails server
$ cd twitter_demo
$ mix deps.get && npm install
$ mix e...
ROUTES
# /web/router.ex
defmodule TwitterDemo.Router do
use TwitterDemo.Web, :router
pipeline :browser do
plug :accepts, ["html"]...
# /web/router.ex
defmodule TwitterDemo.Router do
use TwitterDemo.Web, :router
pipeline :browser do
plug :accepts, ["html"]...
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 w...
CONTROLLER
# /web/router.ex
defmodule TwitterDemo.Router do
use TwitterDemo.Web, :router
pipeline :browser do
plug :accepts, ["html"]...
# /app/controllers/page_controller.rb
class PageController < ApplicationController
def index
end
def timeline
end
end
# /w...
# /app/controllers/page_controller.rb
class PageController < ApplicationController
def index
end
def timeline
end
end
# /w...
Typical code in OOP / imperative programming:
people = DB.find_customers
orders = Orders.for_customers(people)
tax = sales...
Pipe Operator |>
With Elixir pipe operator we can do just
filing = DB.find_customers
|> Orders.for_customers
|> sales_tax(...
VIEWS /
TEMPLATES
<!-- /app/views/page/index.html.erb -->
<h1>Welcome to TwitterDemo!</h1>
<%= form_tag timeline_path, method: "get" do %>
<...
<!-- /app/views/page/timeline.html.erb -->
<script>window.nickname = "<%= @nickname %>";</script>
<div id="messages"></div...
MODEL
$ rails g model Message author:string
content:text
$ rake db:migrate
$ mix phoenix.gen.model Message messages
author:strin...
# /web/models/message.ex
defmodule TwitterDemo.Message do
use TwitterDemo.Web, :model
@derive {Poison.Encoder, only: [:aut...
# /web/models/message.ex
defmodule TwitterDemo.Message do
use TwitterDemo.Web, :model
@derive {Poison.Encoder, only: [:aut...
Ecto
You could think about Ecto as “the ActiveRecord of Elixir”.
But better don’t. It’s not even an ORM (in its purest def...
CHANNEL
$ 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 loo...
# /web/channels/user_socket.ex
defmodule TwitterDemo.UserSocket do
use Phoenix.Socket
## Channels
channel "timeline:lobby"...
# /web/channels/timeline_channel.ex
defmodule TwitterDemo.TimelineChannel do
use TwitterDemo.Web, :channel
alias TwitterDe...
# /web/channels/timeline_channel.ex
defmodule TwitterDemo.TimelineChannel do
use TwitterDemo.Web, :channel
alias TwitterDe...
Pattern Matching
In Elixir: a = 1 does not mean we are assigning 1 to the variable a.
Instead of assigning a variable, in ...
Pattern Matching
Function signatures use pattern matching.
Therefore we can have more than one signature.
defmodule Factor...
# /web/channels/timeline_channel.ex
defmodule TwitterDemo.TimelineChannel do
use TwitterDemo.Web, :channel
alias TwitterDe...
JAVASCRIPT
# /app/assets/javascripts/channels/timeline.coffee
App.timeline = App.cable.subscriptions.create {channel:
"TimelineChanne...
HOMEWORK
(for you)
1. Send history of messages when connecting to
channel.
2. Add Presence module (to display who is online).
3. Create a sta...
tl;dr
$ rails new my_project $ mix phoenix.new my_project
$ rails g [x] $ mix phoenix.gen.[x]
$ bundle install $ mix deps....
LONG TERM
PRODUCTIVY
EXPLICIT > IMPLICIT
or at least some reasonable balance
“Functional Programming is about
making the complex parts of your
program explicit”
– José Valim
Next steps (for you)
• Watch every talk by José Valim & Chris McCord
Really, you won’t regret.
• Books:
Programming Elixir...
THANK YOU
Questions?
Special thanks go to Diacode’s former team:
Victor Viruete, Ricardo García, Artur Chruszcz & Bruno Ba...
Phoenix for Rails Devs
Phoenix for Rails Devs
Phoenix for Rails Devs
Phoenix for Rails Devs
Phoenix for Rails Devs
Phoenix for Rails Devs
Phoenix for Rails Devs
Nächste SlideShare
Wird geladen in …5
×

Phoenix for Rails Devs

638 Aufrufe

Veröffentlicht am

Slides for "Phoenix for Rails dev" talk at Conferencia Rails Madrid 2016 (conferenciaror.es).

Veröffentlicht in: Software
  • Als Erste(r) kommentieren

Phoenix for Rails Devs

  1. 1. PHOENIX FOR RAILS DEVS Conferencia Rails Madrid 15/10/2016
  2. 2. If you're having talk problems, I feel bad for you, son. I got 61 problems, but a slide ain't one; hit me!
  3. 3. About me: Javier Cuevas @javier_dev RUBY ON RAILS SHOP WHO EMBRACED ELIXIR AIRBNB FOR DOGS “MAJESTIC” RAILS 3.2 MONOLITH
  4. 4. GUESTS OF THE DAY
  5. 5. 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
  6. 6. 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!
  7. 7. WHAT IS ELIXIR?
  8. 8. 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.
  9. 9. WHAT IS PHOENIX?
  10. 10. Phoenix is a productive web framework for Elixir that does not compromise speed and maintainability.
  11. 11. PHOENIX = PRODUCTIVITY + PERFORMANCE
  12. 12. PERFORMANCE
  13. 13. I don’t care about performance. * that much
  14. 14. PRODUCTIVITY SHORT TERM LONG TERM
  15. 15. SHORT TERM PRODUCTIVITY
  16. 16. The Phoenix Backpack • Mix (generators, tasks, etc.) • Erlang libraries + Hex.pm • ES6 out of the box • Live reload • Nice error pages • Concurrent test tools + DocTests • Great docs (for real) • Channels + Presence • OTP: humongous set of libraries for distributed computing • Erlang observer • ....
  17. 17. Remember the “15 min blog” by DHH? That was productivity! Let’s try build the “15 min real time Twitter” (or something close to).
  18. 18. https://github.com/javiercr/conferencia_ror_demo
  19. 19. LET’S GET STARTED
  20. 20. rails new twitter_demo mix phoenix.new twitter_demo
  21. 21. !"" twitter_demo #"" app $   #"" assets $   #"" channels $   #"" controllers $   #"" helpers $   #"" jobs $   #"" mailers $   #"" models $   !"" views #"" bin #"" config #"" db #"" lib #"" log #"" public #"" test #"" tmp !"" vendor !"" twitter_demo #"" config #"" deps #"" lib #"" node_modules #"" priv #"" test !"" web #"" channels #"" controllers #"" models #"" static #"" templates !"" views
  22. 22. $ cd twitter_demo $ bundle install $ rake db:create $ rails server $ cd twitter_demo $ mix deps.get && npm install $ mix ecto.create $ mix phoenix.server
  23. 23. ROUTES
  24. 24. # /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
  25. 25. # /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
  26. 26. 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.
  27. 27. CONTROLLER
  28. 28. # /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
  29. 29. # /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
  30. 30. # /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
  31. 31. 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 |>
  32. 32. 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.
  33. 33. VIEWS / TEMPLATES
  34. 34. <!-- /app/views/page/index.html.erb --> <h1>Welcome to TwitterDemo!</h1> <%= form_tag timeline_path, method: "get" do %> <label for="nickname">Nickname</label>: <input type="text" name="nickname"></input> <button>Connect!</button> <% end %> <!-- /web/templates/page/index.html.eex --> <h1>Welcome to TwitterDemo!</h1> <%= form_tag(page_path(@conn, :timeline), method: "get") do %> <label for="nickname">Nickname</label>: <input type="text" name="nickname"></input> <button>Connect!</button> <% end %>
  35. 35. <!-- /app/views/page/timeline.html.erb --> <script>window.nickname = "<%= @nickname %>";</script> <div id="messages"></div> <input id="chat-input" type="text"></input> <!-- /web/templates/page/timeline.html.eex --> <script>window.nickname = "<%= @nickname %>";</script> <div id="messages"></div> <input id="chat-input" type="text"></input>
  36. 36. MODEL
  37. 37. $ rails g model Message author:string content:text $ rake db:migrate $ mix phoenix.gen.model Message messages author:string content:text $ mix ecto.create && mix ecto.migrate
  38. 38. # /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
  39. 39. # /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
  40. 40. 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.
  41. 41. CHANNEL
  42. 42. $ rails g channel Timeline new_msg $ mix phoenix.gen.channel Timeline
  43. 43. # /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
  44. 44. # /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
  45. 45. # /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
  46. 46. # /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
  47. 47. 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
  48. 48. 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
  49. 49. # /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!
  50. 50. JAVASCRIPT
  51. 51. # /app/assets/javascripts/channels/timeline.coffee App.timeline = App.cable.subscriptions.create {channel: "TimelineChannel", nickname: window.nickname}, connected: -> # Called when the subscription is ready for use on the server chatInput = document.querySelector("#chat-input") chatInput.addEventListener "keypress", (event) => if event.keyCode == 13 @new_msg chatInput.value chatInput.value = "" received: (payload) -> @_renderdMessage(payload.message) new_msg: (message) -> @perform 'new_msg', {content: message} _renderdMessage: (message) -> # [...] messagesContainer.appendChild(messageItem) // /web/static/js/socket.js import {Socket} from "phoenix" let socket = new Socket("/socket", { params: { token: window.userToken, nickname: window.nickname }}) socket.connect() let channel = socket.channel("timeline:lobby", {}) let chatInput = document.querySelector("#chat-input") let renderMessage = (message) => { // [...] messagesContainer.appendChild(messageItem) } chatInput.addEventListener("keypress", event => { if(event.keyCode === 13){ channel.push("new_msg", {content: chatInput.value}) chatInput.value = "" } }) channel.on("new_msg", payload => { renderMessage(payload.message) }) channel.join() export default socket
  52. 52. HOMEWORK (for you)
  53. 53. 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
  54. 54. tl;dr $ rails new my_project $ mix phoenix.new my_project $ rails g [x] $ mix phoenix.gen.[x] $ bundle install $ mix deps.get $ rake db:migrate $ mix ecto.migrate $ rails server $ mix phoenix.server $ rails console $ iex -S mix bundle + rake Mix RubyGems Hex.pm Rack Plug Minitest / RSpec ExUnit ActiveRecord Ecto ActionCable Channels + Presence sprockets Brunch (npm based) Redis / Sidekiq / Resque OTP
  55. 55. LONG TERM PRODUCTIVY
  56. 56. EXPLICIT > IMPLICIT or at least some reasonable balance
  57. 57. “Functional Programming is about making the complex parts of your program explicit” – José Valim
  58. 58. 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/
  59. 59. 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 ]

×