Workshop 4: Runtime
Repaso procesos
Con las técnicas aprendidas creamos un contador simple
defmodule Counter do
def start_link do
Task.start_link(fn -> loop(0) end)
end
defp loop(state) do
receive do
{:get, caller} ->
send caller, state
loop(state)
:inc ->
new_state = state + 1
loop(new_state)
end
end
end
Y lo puedo llamar:
{:ok, pid} = Counter.start_link()
send(pid, {:get, self()})
value = receive do: (x -> x)
send(pid, :inc)
send(pid, {:get, self()})
value = receive do: (x -> x)
Implementamos como Genserver
defmodule Counter do
use GenServer
def init(value) do
{:ok, value}
end
def handle_call(:get, _, state) do
{:reply, state, state}
end
def handle_cast(:inc, state) do
{:noreply, state + 1}
end
end
Y lo llamamos:
# Start the server
{:ok, pid} = GenServer.start_link(Counter, 0)
GenServer.call(pid, :get)
GenServer.cast(pid, :inc)
GenServer.call(pid, :get)
Cliente y Servidor
Pero la idea es abstraernos de que es un GenServer. Implementamos un lado Cliente y un lado Servidor:
defmodule Counter do
use GenServer
# Cliente
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, 0, opts)
end
def get(pid) do
GenServer.call(pid, :get)
end
def inc(pid) do
GenServer.cast(pid, :inc)
end
# Servidor
def init(value) do
{:ok, value}
end
def handle_call(:get, _, state) do
{:reply, state, state}
end
def handle_cast(:inc, state) do
{:noreply, state + 1}
end
end
Y lo llamamos:
{:ok, pid} = Counter.start_link()
Counter.get(pid)
Counter.inc(pid)
Counter.get(pid)
Los procesos pueden ser nombrados:
GenServer.start_link(Counter, 0, name: :contador)
Process.whereis(:contador)
Si no queremos usar PID podemos darle un nombre local:
Counter.start_link(name: :counter)
Counter.get(:counter)
Counter.inc(:counter)
Counter.get(:counter)
Repaso procesos
Los procesos están aislados entre sí:
spawn(fn ->
spawn(fn ->
Process.sleep(1000)
IO.puts("Internal process finished!")
end)
raise("Something went wrong!")
end)
Pero también se pueden linkear:
spawn(fn ->
spawn_link(fn ->
Process.sleep(1000)
IO.puts("Internal process finished!")
end)
raise("Something went wrong!")
end)
Al linkearlos, si uno falla, manda una señal de salida (EXIT) al otro proceso. Todos los procesos mueren.
Se pueden atrapar las señales se salida (trapping exit):
spawn(fn ->
Process.flag(:trap_exit, true)
spawn_link(fn -> raise("Hubo un error") end)
receive do
msg -> IO.inspect(msg)
end
end)
Supervisión
Antes de ver supervición, vamos a agregar nuestro contador al código de nuestra aplicación Phoenix.
Agregamos el código al archivo lib/blog/counter.ex
.
Y nombramos al módulo Yo.Blog.Counter
.
Y agregamos al archivo .iex.exs
el alias Yo.Blog.Counter
.
Abrimos iex -S mix1
y corremos:
children = [{Counter, [name: :counter]}]
Supervisor.start_link(children, strategy: :one_for_one)
Y luego comprobamos que el proceso Contador está corriendo:
pid = Process.whereis(:counter)
Counter.get(:counter)
Counter.inc(:counter)
Counter.get(:counter)
Matamos al proceso, y comprobamos que el supervisor lo revivió en el estado original:
Process.exit(pid, :kill)
pid = Process.whereis(:counter)
Counter.get(:counter)
Corremos un supervisor con dos contadores de hijo:
children = [
Supervisor.child_spec({Counter, [name: :counter1]}, id: :counter1),
Supervisor.child_spec({Counter, [name: :counter2]}, id: :counter2)
]
Supervisor.start_link(children, strategy: :one_for_one)
Y comprobamos que los podemos llamar y que al matar uno, el otro sigue vivo:
pid1 = Process.whereis(:counter1)
pid2 = Process.whereis(:counter2)
Process.exit(pid1, :kill)
Process.whereis(:counter1)
Process.whereis(:counter2)
Tarea para casa:
Hacer lo mismo pero con estrategias :one_for_all
y :rest_for_one
.
Vamos al archivo application.ex
:
Y agregamos nuestro Contador en el árbol de supervisión:
children = [
Yo.Repo,
YoWeb.Endpoint,
{Yo.Blog.Counter, name: :counter}
]
Corremos iex -S mix
y lo probamos:
Counter.get(:counter)
Counter.inc(:counter)
Agregamos en nuestro contexto blog.ex
:
alias Yo.Blog.Counter
def get_counter() do
Counter.get(:counter)
end
def increment_counter() do
Counter.inc(:counter)
end
Creamos un nuevo Plug para contar en yo_web/plugs/count.ex
:
defmodule YoWeb.Plugs.Count do
import Plug.Conn
alias Yo.Blog
def init(default), do: default
def call(conn, options) do
Blog.increment_counter()
new_count = Blog.get_counter()
Plug.Conn.assign(conn, :counter, new_count)
end
end
Agregamos el plug en el pipeline browser
, en router.ex
:
pipeline :browser do
# ...
plug YoWeb.Plugs.Count
end
Y finalmente dentro del header, en templates/layouts/app.html.eex
agregamos:
Views: <%= @conn.assigns.counter %>
Visitamos la página con mix phx.server
y vemos los resultados.
Agregamos formulario de creacion de Comentarios.
1. Agregar la nueva ruta:
POST /post/:id/comments
resources "/posts", PostController do
resources "/comments", CommentController, only: [:create]
end
2. Agregamos el formulario
# lib/yo_web/templates/comment/form.html.eex
<%= form_for @comment_changeset, @action, fn f -> %>
<%= if @comment_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :body %>
<%= text_input f, :body %>
<%= error_tag f, :body %>
<%= hidden_input f, :post_id, value: @post.id %>
<div>
<%= submit "Save" %>
</div>
<% end %>
3. Modificar el template del post show.html.eex
con el formulario nuevo y el
listado
<hr>
<%= for comment <- @post.comments do %>
<p>
<div class=alert-info><%= comment.body %></div>
</p>
<% end %>
<h3> New comment <h3>
<%= render YoWeb.CommentView, "form.html",
Map.put(assigns, :action, Routes.post_comment_path(@conn, :create, 1)) %>
4. Agregar el controlador CommentController
con el método create
defmodule YoWeb.CommentController do
use YoWeb, :controller
alias Yo.Blog
alias Yo.Blog.Post
def create(conn, %{"comment" => comment_params, "post_id" => post_id}) do
post = Blog.get_post!(post_id)
case Blog.create_comment(comment_params) do
{:ok, comment} ->
conn
|> put_flash(:info, "Comment created successfully.")
|> redirect(to: Routes.post_path(conn, :show, post_id))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, YoWeb.PostView, "show.html", post: post, comment_changeset: changeset)
end
end
end
5. Implementamos los métodos necesarios en nuestra contexto
@doc """
Returns an `%Ecto.Changeset{}` for traking comment changes.
## Examples
iex> change_comment(comment)
%Ecto.Changeset{source: %Comment{}}
"""
def change_comment(%Comment{} = comment) do
Comment.changeset(comment, %{})
end
@doc """
Creates a comment belongs_to a post.
## Examples
iex> create_comment(%{field: value})
{:ok, %Post{}}
iex> create_comment(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_comment(attrs \\ %{}) do
%Comment{}
|> Comment.changeset(attrs)
|> Repo.insert()
end
6. Creamos el Modulo CommentView
para usar el render
defmodule YoWeb.CommentView do
use YoWeb, :view
end
Protocols
- Medio para lograr polimorfismo en Elixir
- Los protocolos son especificamente para cuando quiere cambiar el comportamiento dependiendo del tipo de un dato
Ejemplo String.Chars
iex> to_string(5)
"5"
iex> to_string(12.4)
"12.4"
iex> to_string("foo")
"foo"
Para una tupla?
to_string({:foo})
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:foo}
(elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir) lib/string/chars.ex:17: String.Chars.to_string/1
Implementemos para tupla
defimpl String.Chars, for: Tuple do
def to_string(tuple) do
interior =
tuple
|> Tuple.to_list()
|> Enum.map(&Kernel.to_string/1)
|> Enum.join(", ")
"{#{interior}}"
end
end
iex> to_string({3.14, "apple", :pie})
"{3.14, apple, pie}"
Implementemos un Protocolo
defprotocol AsAtom do
def to_atom(data)
end
defimpl AsAtom, for: Atom do
def to_atom(atom), do: atom
end
defimpl AsAtom, for: BitString do
defdelegate to_atom(string), to: String
end
defimpl AsAtom, for: List do
defdelegate to_atom(list), to: List
end
defimpl AsAtom, for: Map do
def to_atom(map), do: List.first(Map.keys(map))
end
iex> import AsAtom
AsAtom
iex> to_atom("string")
:string
iex> to_atom(:an_atom)
:an_atom
iex> to_atom([1, 2])
:"\x01\x02"
iex> to_atom(%{foo: "bar"})
:foo
Si hay tiempo, creamos aplicación Nueva
mix new contador --sup
Cosas que no dimos en el curso:
Streams
Son enumerables Lazy que se pueden componer:
1..10000 |> Stream.map(&(&1 * &1)) |> Enum.take(10)
Útiles para trabajar con el concepto de infinito. También útiles para trabajar con archivos grandes sin ocupar toda la memoria:
"./my_file.txt"
|> File.stream!
|> Stream.map(&String.strip/1)
|> Stream.with_index
|> Stream.map(fn {line, i} -> "#{i}: #{line}" end)
|> Enum.take(1)
|> IO.inspect()
With
A veces no es siempre fácil escribir el código con Pipes.
Para estos casos se puede usar with
:
with {:ok, data} <- Reader.read(socket),
{:ok, command} <- Parser.parse(data),
do: Server.run(command)
Typespecs
Elixir es dinámico, así que no tiene chequeo de tipos en tiempo de compilación.
Pueden agregar especificaciones de tipo con la anotación spec
:
@spec round(number) :: integer
def round(number) do
:erlang.round(number)
end
Y una herramienta como Dialyzer va a usarlos para analizar el código.
Doctests
Las anotaciones @doc
se usan para documentar las funciones.
Si se le agregan ejemplos con esta notación se pueden correr en los tests:
@doc ~S"""
Parses the given `line` into a command.
## Examples
iex> KVServer.Command.parse("CREATE shopping\r\n")
{:ok, {:create, "shopping"}}
"""
def parse(_line) do
:not_implemented
end
Intercompatibilidad con Erlang
Pueden usar librerías de Erlang desde Elixir. Los módulos de Erlang se escriben como átomos:
:ets.new(:a_name, [])
:mnesia.create_schema([node()])
:observer.start
:queue.new
:rand.uniform()
Canales, Presence y Phoenix Live View
Phoenix tiene un excelente soporte para websockets.
- Con Canales podemos crear chats en tiempo real.
- Con Presence podemos saber quien está conectado en tiempo real.
- Con Phoenix Live View podemos crear aplicaciones super ricas sin escribir una sóla linea de javascript.
Nerves
Herramienta para construir sistemas embebidos (Raspberries, etc.).
Programación distribuída
Es uno de los puntos fuertes de Erlang y Elixir. Son problemas complejos. Elixir provee las abstracciones adecuadas.
Palabras finales
La web está evolucionando. La web moderna es altamente conectada y necesita sistemas en tiempo real. Necesita incluir dispositivos conectados. Necesita sistemas de alta disponibilidad, escalables, tolerantes a fallos y distribuidos. Necesita sistemas que puedan aprovechar al máximo la capacidad de procesamiento de las computadoras.
Erlang, Elixir y Phoenix fueron creados para resolver este tipo de problemas.