Elixir



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, while also being successfully used in web development and the embedded software domain.


Overview


Currently this is intended just to be collection of notes as I attempt to learn elixir. More structure will be added as time goes by.

Following on from some elixir training on Udemy here is the code from a small project to create a identicon (similar to those created by github).

There are lots of good learning resources across the web Here are some that I constantly refer to :-.

Top5


I as learning exercise I have created a version of Top5 (a simple task manager) that I originally developed using Pharo (Smalltalk).

This version of Top5 uses the Phoenix web framework..

Ecto


Ecto is the database wrapper and query generator for Elixir. It provides a standardised API and a set of abstractions for talking to all the different kinds of databases, so that Elixir developers can query whatever database they’re using by employing similar constructs.

A getting started guide can be found here.

Changesets

Much more detail is given in the Ecto guides but a changeset allows developers to filter, cast, and validate changes before they are applied to some persistence store.

An example of working with a changeset can be seen here based on a User schema with the Accounts context (assumes that within iex - alias Top5.Accounts.User has been run.) :-

%User{}
  %Top5.Accounts.User{
  __meta__: #Ecto.Schema.Metadata<:built, "users">,
  email: nil,
  id: nil,
  inserted_at: nil,
  password: nil,
  updated_at: nil,
  username: nil
}                    

The changeset indicates that the data supplied is invalid (false) as all fields are currently blank.

User.changeset(%User{}, %{})
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [
    username: {"can't be blank", [validation: :required]},
    email: {"can't be blank", [validation: :required]},
    password: {"can't be blank", [validation: :required]}
  ],
  data: #Top5.Accounts.User<>,
  valid?: false                    

The changeset indicates that the data supplied is still invalid (false) but the username field has been supplied.

User.changeset(%User{}, %{:username => "Richard"})
#Ecto.Changeset<
  action: nil,
  changes: %{username: "Richard"},
  errors: [
    email: {"can't be blank", [validation: :required]},
    password: {"can't be blank", [validation: :required]}
  ],
  data: #Top5.Accounts.User<>,
  valid?: false                    

Query

The module Ecto.Query provides the query DSL (Domain Specific Language) for a Repo so that it become possible to retrieve and manipulate data.

Here are some examples run within iex (the elixir shell). It should be assumed that modules Ecto.Query, the appropriate Repo module etc have already been imported / aliased :-

Perform a count - "select count(*) from tasks;.

Repo.aggregate( ( from t in Task), :count, :id )
[debug] QUERY OK source="tasks" db=4.3ms queue=0.1ms
SELECT count(t0."id") from tasks AS t0 []
5                 
Repo.one( from t in Task, select: count(t.id), where: t.user_id ==1 )
[debug] QUERY OK source="tasks" db=4.9ms
SELECT count(t0."id") from tasks AS t0 where (t0."user_id" = 1) []
5                 

ETS


Erlang Term Storage, commonly referred to as ETS, is a powerful storage engine built into OTP and available to use in Elixir. For more info (beyond these small examples) try - Elixir School.

Create a table - product.

:ets.new(:product, [:set, :protected, :named_table])                

To add data into the product table.

:ets.insert(:product, {"Apple", [0.22]})
:ets.insert(:product, {"Banana", [0.24]})

Using the Basket example (shown below) it is then possible to retrieve this data and add relevant entries to a virtual shopping card.

{:ok, p1} = Basket.start_link([])
Basket.add(p1, "Apple")
Basket.add(p1, "Banana")

List what is currently in the Basket.

Basket.list(p1)
[
  %{added: "2019-09-11 12:07:22.537378Z", price: 0.24, product: "Banana"},
  %{added: "2019-09-11 12:07:15.317793Z", price: 0.22, product: "Apple"}
]

Remove an item from the Basket.

Basket.remove(p1, %{added: "2019-09-11 12:07:22.537378Z", price: 0.24, product: "Banana"})
Basket.list(p1)
[%{added: "2019-09-11 12:07:15.317793Z", price: 0.22, product: "Apple"}]

The code for Basket.ex.

defmodule Basket do
  use GenServer

  # GenServer Client

  def start_link(default) when is_list(default) do
    GenServer.start_link(__MODULE__, default)
  end

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

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

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

  # GenServer callbacks

  def init(basket) do
    {:ok, basket}
  end

  def handle_cast({:add, product}, basket) do
    case :ets.lookup(:product, product) do
      [{_, [price] }] -> 
        {:ok, dt} = DateTime.now("Etc/UTC") 
        {:noreply, [%{product: product, price: price, added: DateTime.to_string(dt) } | basket]}
      _ -> 
        {:noreply, basket}
    end
  end

  def handle_cast({:remove, item}, basket) do
    updated_basket = Enum.reject(basket, fn x -> x == item end)
    {:noreply, updated_basket}
  end

  def handle_call(:list, _, basket) do
    {:reply, basket, basket}
  end
end