Skip to content

Latest commit

 

History

History
692 lines (554 loc) · 12.7 KB

index.org

File metadata and controls

692 lines (554 loc) · 12.7 KB

Fun with Elixir

Dafuq is Elixir?

./images/dafuq_meme.jpg

Elixir is a dynamic, functional language designed for building scalable and maintainable applications.

Basic Elixir

https://elixir-lang.org/install.html

https://github.com/Ironjanowar/TallerElixir

Pattern matching

x = 1
# 1

1 = x
# 1

1 = y
# ** (CompileError) iex:3: undefined function y/0

2 = x
# ** (MatchError) no match of right hand side value: 1 ...

Pattern matching

{a, b} = {1, 2}
# {1, 2}

a
# 1

{a, b} = {1, 2, 3}
# ** (MatchError) no match of right hand side value: {1, 2, 3} ...

{c, _, _} = {3, 3, 6}
# {3, 3, 6}

c
# 3

Atoms

:ok
:hello
:this_is_an_atom

ThisIsAlsoAnAtom
Hello

case clause

case {1, 2, 3} do
  {4, 5, 6} -> "Does not match"
  {1, x, 3} -> "Match and bind x to 2"
  _ -> "This will match any other value"
end
# "Match and bind x to 2"

case clause

Match the value of a variable

x = 2

case 2 do
  ^x -> "Will match"
  _ -> "This will match any other value"
end
# "Will match"

case clause

Guards

case {1, 2, 3} do
  {1, x, 3} when x > 0 -> "Will match"
  _ -> "This will match any other value"
end
# "Will match"

case clause

Atoms are useful here

case some_function() do
  {:ok, result} -> # Do something with result
  {:error, error_message} -> # Do something with error_message
end

Module definition and functions

# le_test.ex
defmodule LeTest do
  def sum(a, b) do
    a + b
  end

  def sum({a, b}) do
    a + b
  end
end

Module definition and functions

# le_test.ex
defmodule LeTest do
  # Arity 2
  def sum(a, b), do: a + b

  # Arity 1
  def sum({a, b}), do: a + b
  def sum(a), do: a + 1
  def sum(_), do: 0
end

Module definition and functions

LeTest.sum(1, 2)
# 3

LeTest.sum(1)
# 2

LeTest.sum({2, 2})
# 4

Module definition and functions

Guards are very useful in functions

# le_test.ex
defmodule LeTest do
  def sum(a, b) when is_integer(a) and is_integer(b), do: {:ok, a + b}
  def sum(_, _), do: {:error, "I only accept integers"}
end

Anonymous functions

sum = fn a, b -> a + b end
# #Function<20.128620087/0 in :erl_eval.expr/5>

sum.(1, 2)
# 3

sum = & &1 + &2

sum.(2, 2)
# 4

Anonymous functions

Enum.map([1, 2, 3], fn x -> x + 1 end)
# [2, 3, 4]

Enum.map([1, 2, 3], & &1 + 1)
# [2, 3, 4]

Pipe operator

[1, 2, 3] |> Enum.map(fn x -> x + 1 end)
# [2, 3, 4]

[1, 2, 3] |> Enum.map(fn x -> x + 1 end) |> Enum.sum()
# 9

[1, 2, 3]
|> Enum.map(fn x -> x + 1 end)
|> Enum.map(fn x -> x * 2 end)
|> Enum.sum()
|> Integer.to_string()
# "18"

Processes

Create processes

spawn(fn -> nil end)
# #PID<0.214.0>

Create processes

spawn(fn -> Enum.sum(1..100) end)
# #PID<0.220.0>

Send messages

father = self()
spawn(fn ->
  sum = Enum.sum(1..100)
  send(father, sum)
end)
# #PID<0.231.0>

flush
# 5050
# :ok

Receive messages

father = self()
spawn(fn ->
  sum = Enum.sum(1..100)
  send(father, sum)
end)

receive do
  num when is_integer(num) -> "Received the result #{num}"
  _ -> "Wut?"
end
# "Received the result 5050"

Receive messages

pid =
  spawn(fn ->
    receive do
      {pid, :ping} ->
        send(pid, :pong)
    end
  end)
# #PID<0.245.0>

Process.alive?(pid)
# true

send(pid, {self(), :ping})
# {#PID<0.180.0>, :ping}

flush
# :pong
# :ok

Process.alive?(pid)
# false

Receive messages

defmodule PingPong do
  def loop() do
    receive do
      {pid, :ping} ->
        send(pid, :pong)
        loop()

      {pid, :pong} ->
        send(pid, :ping)
        loop()
    end
  end
end

Maintain a state

defmodule PingPong do
  def loop(), do: loop({0, 0})
  def loop({pings, pongs} = state) do
    receive do
      {pid, :ping} ->
        send(pid, :pong)
        loop({pings + 1, pongs})

      {pid, :pong} ->
        send(pid, :ping)
        loop({pings, pongs + 1})
      {pid, :state} ->
        send(pid, state)
        loop(state)
    end
  end
end

Link a process

self()
# #PID<0.101.0>

pid = spawn(fn -> receive do :crash -> 1/0 end end)
# #PID<0.140.0>

Process.link(pid)
# true

send(pid, :crash)
# 18:37:21.684 [error] Process #PID<0.140.0> raised an exception
# ** (ArithmeticError) bad argument in arithmetic expression
#     :erlang./(1, 0)

self()
# #PID<0.115.0>

Link a process

Handle crashes, the magic :trap_exit

self()
# #PID<0.115.0>

Process.flag(:trap_exit, true)
# false

pid = spawn(fn -> receive do :crash -> 1/0 end end)
# #PID<0.140.0>

Process.link(pid)
# true

send(pid, :crash)
# 18:37:21.684 [error] Process #PID<0.140.0> raised an exception
# ** (ArithmeticError) bad argument in arithmetic expression
#     :erlang./(1, 0)

self()
# #PID<0.115.0>

flush
# {:EXIT, #PID<0.140.0>, {:badarith, [{:erlang, :/, [1, 0], []}]}}
# :ok

Monitor a process

{pid, _} = spawn_monitor(fn -> receive do :crash -> 1/0 end end)
# {#PID<0.109.0>, #Reference<0.1454498977.3767533571.36942>}

send(pid, :crash)
# 01:04:55.908 [error] Process #PID<0.112.0> raised an exception
# ** (ArithmeticError) bad argument in arithmetic expression
#     :erlang./(1, 0)

GenServer

A GenServer is a process like any other Elixir process and it can be used to keep state, execute code asynchronously and so on.

GenServer

defmodule Stack do
  use GenServer

  # Callbacks
  def init(stack) do
    {:ok, stack}
  end

  def handle_call(:pop, _from, [head | tail]) do
    {:reply, head, tail}
  end

  def handle_cast({:push, item}, state) do
    {:noreply, [item | state]}
  end
end

Create GenServers

{:ok, pid} = GenServer.start_link(Stack, [:hello])
# {:ok, #PID<0.129.0>}

state: [:hello]

Send messages

GenServer.cast(pid, {:push, :world})
# :ok

Receive messages

def handle_cast({:push, item}, state) do
  {:noreply, [item | state]}
end

Send messages

GenServer.call(pid, :pop)
# :world

Receive messages

def handle_call(:pop, _from, [head | tail]) do
  {:reply, head, tail}
end

GenServer answers

def handle_call(message, from, state) do
  # Replies
  {:reply, answer, new_state}
  {:noreply, new_state}

  {:stop, reason, reply, new_state}
  {:stop, reason, reply}
end

GenServer answers

def handle_cast(message, state) do
  # Replies
  {:noreply, new_state}
  {:stop, reason}
end

Create a client for a GenServer

defmodule Stack do
  use GenServer

  # Client
  def start(initial_state) do
    GenServer.start_link(Stack, initial_state)
  end

  def push(pid, item) do
    GenServer.cast(pid, {:push, item})
  end

  def pop(pid) do
    GenServer.call(pid, :pop)
  end

  # Callbacks ...
end

Name a GenServer

defmodule Stack do
  use GenServer

  # Client
  def start(initial_state \\ []) do
    GenServer.start_link(Stack, initial_state, name: Stack)
  end

  def push(item) do
    GenServer.cast(Stack, {:push, item})
  end

  def pop() do
    GenServer.call(Stack, :pop)
  end

  # Callbacks ...
end

Agents

The Agent module provides a basic server implementation that allows state to be retrieved and updated via a simple API.

Agents

defmodule Basket do
  use Agent

  def start() do
    Agent.start_link(&MapSet.new/0, name: __MODULE__)
  end

  def member?(product) do
    Agent.get(__MODULE__, fn state -> MapSet.member?(state, product) end)
  end

  def get() do
    Agent.get(__MODULE__, fn state -> state end)
  end

  def put(product) do
    Agent.update(__MODULE__, fn state -> MapSet.put(state, product) end)
  end

  def delete(product) do
    Agent.update(__MODULE__, fn state -> MapSet.delete(state, product) end)
  end
end

Agents

defmodule Basket do
  use Agent

  def start() do
    Agent.start_link(&MapSet.new/0, name: __MODULE__)
  end

  def member?(product) do
    Agent.get(__MODULE__, &MapSet.member?(&1, product))
  end

  def get() do
    Agent.get(__MODULE__, &(&1 |> MapSet.to_list()))
  end

  def put(product) do
    Agent.update(__MODULE__, &MapSet.put(&1, product))
  end

  def delete(product) do
    Agent.update(__MODULE__, &(MapSet.delete(&1, product) |> MapSet.to_list()))
  end
end

Telegram Bots (Practice)

Creating an Elixir project

$ mix new awesome_bot
 * creating README.md
 * creating .formatter.exs
 * creating .gitignore
 * creating mix.exs
 * creating config
 * creating config/config.exs
 * creating lib
 * creating lib/awesome_bot.ex
 * creating test
 * creating test/test_helper.exs
 * creating test/awesome_bot_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd awesome_bot
    mix test

Run "mix help" for more commands.

Adding dependencies

https://github.com/rockneurotiko/ex_gram

# awesome_bot/mix.exs

# Run "mix help deps" to learn about dependencies.
defp deps do
  [
    {:ex_gram, "0.5.0-rc3"}
  ]
end

Adding dependencies

https://github.com/rockneurotiko/ex_gram

# awesome_bot/mix.exs

# Run "mix help deps" to learn about dependencies.
defp deps do
  [
    {:ex_gram, git: "git://github.com/rockneurotiko/ex_gram.git"}
  ]
end

Application

defmodule AwesomeBot do
  use Application

  import Supervisor.Spec

  require Logger

  def start(_type, _args) do
    token = ExGram.Config.get(:ex_gram, :token)

    children = [
      supervisor(ExGram, []),
      supervisor(Awesome.Bot, [:polling, token])
    ]

    opts = [strategy: :one_for_one, name: AwesomeBot]

    case Supervisor.start_link(children, opts) do
      {:ok, _} = ok ->
        Logger.info("Starting AwesomeBot")
        ok

      error ->
        Logger.error("Error starting AwesomeBot")
        error
    end
  end
end

Starting module

# Run "mix help compile.app" to learn about applications.
def application do
  [
    extra_applications: [:logger],
    mod: {AwesomeBot, []}
  ]
end

Config, add your token

# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

config :ex_gram,
  token: "token"

Create your message handlers

defmodule AwesomeBot.Bot do
  @bot :awesome_bot

  use ExGram.Bot,
    name: @bot

  def bot(), do: @bot

  def handle({:command, "start", _msg}, context) do
    msg = "Well hello! This is an awesome bot!"
    answer(context, msg)
  end
end

Compile and run your project

$ mix run --no-halt
Compiling 1 file (.ex)

23:57:44.219 [info]  Starting AwesomeBot