There have been good posts written about creating Elixir libraries already. One awesome recent post can be found here. It will walk you throught the process of writing, documenting and publishing your first Elixir library really well.
One thing that was missing for me, however, was a short description on how and why you can wrap your code into reusable OTP application. Let’s fix that.
Two types of Elixir libraries
You probably already noticed that there are two types of libraries in
Elixir. Some libraries expose just modules and their functions, so the
code can be reused between different projects. When you include such
library, you only need to do the following in mix.exs
:
def deps do
[{:uuid, "~> 1.1"}]
end
when you run mix deps.get
, and compile the project, you can start
using UUID
module straight away, right from your code.
Second class of Elixir libraries allow, or even requires, that you
start another OTP application. Popular example is httpoison,
HTTP client library, that asks you to do the following in your
mix.exs
:
def deps do
[{:httpoison, "~> 0.8.0"}]
end
def application do
[applications: [:httpoison]]
end
If your project uses Phoenix web framework, it already starts a few more applications by default:
def application do
[:phoenix, :phoenix_html, :cowboy, :logger, :phoenix_ecto, :postgrex]
end
OTP applications
Creating an Elixir library as an OTP application, allows you to do slightly more than code sharing. An application usually starts it’s own supervision tree. It can be stopped manually whenever needed, and started with different set of parameters without affecting the rest of the system.
Erlang/Elixir can start multiple OTP applications in one instance of VM.
This means OTP applications are loosely coupled, are visible to each
other, and processes between different applications can easily
communicate (provided they know each other’s PIDs or registered names).
When your Elixir project starts, list of applications is
provided by application/0
function as shown above. Those applications
are started in order provided.
You can inspect the list of known and started applications easily using command line, or GUI tools that ship with Erlang.
Command line:
iex(1)> :application.info
[loaded: [{:chatbot, 'chatbot', '0.0.1'}, {:logger, 'logger', '1.2.5'},
{:compiler, 'ERTS CXC 138 10', '6.0.3'}, {:mix, 'mix', '1.2.5'},
{:stdlib, 'ERTS CXC 138 10', '2.8'}, {:iex, 'iex', '1.2.5'},
{:kernel, 'ERTS CXC 138 10', '4.2'}, {:elixir, 'elixir', '1.2.5'}],
loading: [],
started: [chatbot: :temporary, logger: :temporary, mix: :temporary,
iex: :temporary, elixir: :temporary, compiler: :temporary, stdlib:
:permanent,
kernel: :permanent], start_p_false: [],
running: [chatbot: #PID<0.111.0>, logger: #PID<0.102.0>, mix:
#PID<0.66.0>,
iex: #PID<0.48.0>, elixir: #PID<0.41.0>, compiler: :undefined,
stdlib: :undefined, kernel: #PID<0.9.0>], starting: []]
GUI:
:iex(1)> :observer.start()
Applications are also useful if you need to perform extra configuration,
and initialize your library accordingly. For example, logger
can be
configured in your config/config.exs
this way:
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
The configuration can be read by the library using Application.get_env/3.
Each Elixir project will start several applications, including
:logger
, :iex
, and even :elixir
application - those things must be
really handy then!
Let’s get our hands dirty
We will build a (very trendy recently) chat bot. Our chat bot will be Elixir library, that we can share between our projects, or even publish to hex.pm.
First, let’s generate our project, that will be a client for our chatbot:
$> mix new project
mix new project
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/project.ex
* creating test
* creating test/test_helper.exs
* creating test/project_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd project
mix test
Run "mix help" for more commands.
Next, let’s generate our chatbot
application library:
$> mix new chatbot --sup
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/chatbot.ex
* creating test
* creating test/test_helper.exs
* creating test/chatbot_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd chatbot
mix test
Run "mix help" for more commands.
Please note the --sup
in the second invocation. We are telling mix
this way to generate supervision tree for our chatbot
application. If
you compare chatbot/lib/chatbot.ex
with project/lib/project.ex
, you
will notice the former has some extra generated code:
defmodule Chatbot do
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
# Define workers and child supervisors to be supervised
# worker(Chatbot.Worker, [arg1, arg2, arg3]),
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Chatbot.Supervisor]
Supervisor.start_link(children, opts)
end
end
this is scaffold for our application callback module, that by default would start an empty supervision tree.
In both projects, mix.exs
files also differ. Our chatbot points mix
to our application callback module, so it knows what should be started:
def application do
[applications: [:logger],
mod: {Chatbot, []}]
end
Let’s write some AI code ;) for our chat bot:
chatbot/lib/chatbot/ai.ex:
defmodule Chatbot.Ai do
use GenServer
def start_link(name) do
GenServer.start_link(__MODULE__, nil, [name: name])
end
def handle_call(message, _from, nil) do
{:reply, "Hello, stranger! What's your name?", "stranger"}
end
def handle_call(message, _from, "stranger") do
{:reply, "Nice to meet you, #{message}. What you've been up to recently?", message}
end
def handle_call(message, _from, state) do
{:reply, "This is interesting, #{state}! Tell me more on this...", state}
end
end
chatbot/lib/chatbot.ex:
defmodule Chatbot do
def start(_type, _args) do
...
children = [
worker(Chatbot.Ai, [Chatbot.Ai]),
]
...
end
def say(msg) do
GenServer.call Chatbot.Ai, msg
end
end
The code above implements simple chat bot as GenServer and starts it
when chatbot
application starts, under a default supervisor.
Let’s include chat bot library in our project:
project/mix.exs:
def application do
[applications: [:logger, :chatbot]]
end
defp deps do
[{:chatbot, path: "../chatbot"}]
end
If we spawn our project’s interactive elixir shell, we can have a nice chat with our bot:
$> iex -S mix
iex(1)> Chatbot.say "Hi there"
"Hello, stranger! What's your name?"
iex(2)> Chatbot.say "Hubert"
"Nice to meet you, Hubert. What you've been up to recently?"
iex(3)> Chatbot.say "Advanced AI using Elixir"
"This is interesting, Hubert! Tell me more on this..."
By including our chat bot as an application in a separate library, we did not have to take care of intialization in our main project. We can decide to stop and later start our application at any stage, as a unit, from a running project:
iex(4)> :application.stop(:chatbot)
:ok
[info] Application chatbot exited: :stopped
iex(5)> :application.start(:chatbot)
:ok
Summary
OTP applications are a handy way of encapsulating code into logical chunks, such as libraries. If your library needs to perform initialization based on configuration, store state, set up custom supervision tree - this is a way forward.
Post by Hubert Łępicki
Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.