This is a blog of AmberBit - a Elixir and Ruby web development company. Hire us for your project!

Creating Elixir libraries as OTP applications

Hubert

Posted by Hubert Łępicki

Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.
hubert.lepicki@amberbit.com | @hubertlepicki | @hubertlepicki

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()

: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.

Hubert

Hi there!

I hope you enjoyed the blog post. Can we help you with Elixir or Ruby work? We are looking for new opportunities at the very moment, and we do have team available just for you.

Email me at: contact@amberbit.com or use the contact form below.

- Hubert Łępicki

comments powered by Disqus

Want to get in touch? Drop us a line!