In my previous articles we have discussed various Elixir terms and written a hefty amount of code. What we have not discussed, however, is how to structure and organize your code so that it is easy to maintain and release.
Applications are very common for Erlang and Elixir and are used to build reusable components that behave as stand-alone units. One application may have its own supervision tree and configuration, and it can rely on other applications that are available either locally or on some remote server. All in all, working with applications is not that complex, and people who have come, say, from the world of Ruby will find many familiar concepts.
In this article you will learn what applications are, how they can be created, how to specify and install dependencies, and how to provide environment values. At the end of the article we will do some practice and create a web-based calculator.
I will be using Elixir 1.5 in this article (it was released a couple of months ago), but all the explained concepts should apply to version 1.4 as well.
Applications?
Some might argue that the term "application" is not very appropriate because in Erlang and Elixir it actually means a component, or some code that has a bunch of dependencies. The application itself can be used as a dependency as well—in Ruby world we would call it a "gem".
All in all, applications are very common in Elixir and allow you to craft reusable components while also providing easy dependency management. They consist of one or multiple modules with zero or more dependencies and are described by the application resource file. This file contains information about the application's name, version, its modules, dependencies, and some other stuff. You may create the resource file manually, but it is much easier to do so with the mix tool that will also prepare a correct folder structure for you.
So let's see how we can create a new Elixir application!
New Application
To create a new application, all you need to do is run the following command:
mix new app_name
We can also provide the --sup
flag to create an empty supervisor for us. Let's create a new application called Sample
this way:
mix new sample --sup
This command will create a sample directory for you with a handful of files and folders inside. Let me quickly guide you through them:
- config folder contains a sole file config.exs that, as you can guess, provides configuration for the application. Initially it has some useful comments, but no configuration. Note, by the way, that the configuration provided in this file is only restricted to the application itself. If you are loading the application as a dependency, its config.exs will be effectively ignored.
- lib is the primary folder of the application that contains a sample.ex file and a sample folder with an application.ex file. application.ex defines a callback module with a
start/2
function that creates an empty supervisor. - test is the folder containing automated tests for the application. We won't discuss automated tests in this article.
- mix.exs is the file that contains all the necessary information about the application. There are multiple functions here. Inside the
project
function, you provide the app's name (as an atom), version, and environment. Theapplication
function contains information about the application module callback and runtime dependencies. In our case,Sample.Application
is set as the application module callback (that can be treated as the main entry point), and it has to define astart/2
function. As already mentioned above, this function was already created for us by themix
tool. Lastly, thedeps
function lists build-time dependencies.
Dependencies
It is quite important to distinguish between runtime and build-time dependencies. Build-time dependencies are loaded by the mix
tool during the compilation and are basically compiled into your application.
They can be fetched from a service like GitHub, for example, or from the hex.pm website, an external package manager that stores thousands of components for Elixir and Erlang. Runtime dependencies are started before the application starts. They are already compiled and available for us.
There are a couple of ways to specify build-time dependencies in a mix.exs file. If you'd like to use an application from the hex.pm website, simply say:
{:dependency_name, "~> 0.0.1"}
The first argument is always an atom representing the application's name. The second one is the requirement, a version that you desire to use—it is parsed by the Version module. In this example, ~>
means that we wish to download at least version 0.0.1
or higher but less than 0.1.0
. If we say ~> 1.0
, it means we'd like to use version greater than or equal to 1.0
but less than 2.0
. There are also operators like ==
, >
, <
, >=
, and <=
available.
It is also possible to directly specify a :git
or a :path
option:
{:gettext, git: "http://ift.tt/2y1Di4v", tag: "0.1"} {:local_dependency, path: "path/to/local_dependency"}
There is also a :github
shortcut that allows us to provide only the owner's and a repo's name:
{:gettext, github: "elixir-lang/gettext"}
To download and compile all dependencies, run:
mix deps.get
This will install a Hex client if you don't have one and then check if any of the dependencies needs to be updated. For instance, you can specify Poison—a solution to parse JSON—as a dependency like this:
defp deps do [ {:poison, "~> 3.1"} ] end
Then run:
mix deps.get
You will see a similar output:
Running dependency resolution... Dependency resolution completed: poison 3.1.0 * Getting poison (Hex package) Checking package (http://ift.tt/2y2ev01) Fetched package
Poison is now compiled and available on your PC. What's more, a mix.lock file will be created automatically. This file provides the exact versions of the dependencies to use when the application is booted.
To learn more about dependencies, run the following command:
mix help deps
Behaviour Again
Applications are behaviours, just like GenServer and supervisors, which we talked about in the previous articles. As I already mentioned above, we provide a callback module inside the mix.exs file in the following way:
def application do [ mod: {Sample.Application, []} ] end
Sample.Application
is the module's name, whereas []
may contain a list of arguments to pass to the start/2
function. The start/2
function must be implemented in order for the application to boot properly.
The application.ex contains the callback module that looks like this:
defmodule Sample.Application do use Application def start(_type, _args) do children = [ ] opts = [strategy: :one_for_one, name: Sample.Supervisor] Supervisor.start_link(children, opts) end end
The start/2
function must either return {:ok, pid}
(with an optional state as the third item) or {:error, reason}
.
Another thing worth mentioning is that applications do not really require the callback module at all. It means that the application function inside the mix.exs file may become really minimalistic:
def application do [] end
Such applications are called library applications. They do not have any supervision tree but can still be used as dependencies by other applications. One example of a library application would be Poison, which we specified as a dependency in the previous section.
Starting an Application
The easiest way to start your application is to run the following command:
iex -S mix
You will see an output similar to this one:
Compiling 2 files (.ex) Generated sample app
A _build directory will be created inside the sample folder. It will contain .beam files as well as some other files and folders.
If you don't want to start an Elixir shell, another option is to run:
mix run
The problem, though, is that the application will stop as soon as the start
function finishes its job. Therefore, you may provide the --no-halt
key to keep the application running for as long as needed:
mix run --no-halt
The same can be achieved using the elixir
command:
elixir -S mix run --no-halt
Note, however, that the application will stop as soon as you close the terminal where this command was executed. This can be avoided by starting your application in a detached mode:
elixir -S mix run --no-halt --detached
Application Environment
Sometimes you may want the user of an application to set some parameter before the app is actually booted. This is useful when, for example, the user should be able to control which port a web server should listen to. Such parameters can be specified in the application environment that is a simple in-memory key-value storage.
In order to read some parameter, use the fetch_env/2
function that accepts an app and a key:
Application.fetch_env(:sample, :some_key)
If the key cannot be found, an :error
atom is returned. There are also a fetch_env!/2
function that raises an error instead and get_env/3
that may provide a default value.
To store a parameter, use put_env/4
:
Application.put_env(:sample, :key, :value)
The fourth value contains options and is not required to be set.
Lastly, to delete a key, employ the delete_env/3
function:
Application.delete_env(:sample, :key)
How do we provide a value for the environment when starting an app? Well, such parameters are set using the --erl
key in the following way:
iex --erl "-sample key value" -S mix
You can then easily fetch the value:
Application.get_env :sample, :key # => :value
What if a user forgets to specify a parameter when starting the application? Well, most likely we need to provide a default value for such cases. There are two possible places where you can do this: inside the config.exs or inside the mix.exs file.
The first option is the preferred one because config.exs is the file that is actually meant to store various configuration options. If your application has lots of environment parameters, you should definitely stick with config.exs:
use Mix.Config config :sample, key: :value
For a smaller application, however, it is quite okay to provide environment values right inside mix.exs by tweaking the application function:
def application do [ extra_applications: [:logger], mod: {Sample.Application, []}, env: [ # <==== key: :value ] ] end
Example: Creating a Web-Based CalcServer
Okay, in order to see applications in action, let's modify the example that was already discussed in my GenServer and Supervisors articles. This is a simple calculator that allows users to perform various mathematical operations and fetch the result quite easily.
What I want to do is make this calculator web-based, so that we can send POST requests to perform calculations and a GET request to grab the result.
Create a new lib/calc_server.ex file with the following contents:
defmodule Sample.CalcServer do use GenServer def start_link(initial_value) do GenServer.start_link(__MODULE__, initial_value, name: __MODULE__) end def init(initial_value) when is_number(initial_value) do {:ok, initial_value} end def init(_) do {:stop, "The value must be an integer!"} end def add(number) do GenServer.cast(__MODULE__, {:add, number}) end def result do GenServer.call(__MODULE__, :result) end def handle_call(:result, _, state) do {:reply, state, state} end def handle_cast(operation, state) do case operation do {:add, number} -> {:noreply, state + number} _ -> {:stop, "Not implemented", state} end end def terminate(_reason, _state) do IO.puts "The server terminated" end end
We will only add support for the add
operation. All other mathematical operations can be introduced in the same way, so I won't list them here to make the code more compact.
The CalcServer
utilizes GenServer
, so we get child_spec
automatically and can start it from the callback function like this:
def start(_type, _args) do children = [ {Sample.CalcServer, 0} ] opts = [strategy: :one_for_one, name: Sample.Supervisor] Supervisor.start_link(children, opts) end
0
here is the initial result. It must be a number, otherwise CalcServer
will immediately terminate.
Now the question is how do we add web support? To do that, we'll need two third-party dependencies: Plug, which will act as an abstraction library, and Cowboy, which will act as an actual web server. Of course, we need to specify these dependencies inside the mix.exs file:
defp deps do [ {:cowboy, "~> 1.1"}, {:plug, "~> 1.4"} ] end
Now we can start the Plug application under our own supervision tree. Tweak the start function like this:
def start(_type, _args) do children = [ Plug.Adapters.Cowboy.child_spec( :http, Sample.Router, [], [port: Application.fetch_env!(:sample, :port)] ), {Sample.CalcServer, 0} ] # ... end
Here we are providing child_spec
and setting Sample.Router
to respond to requests. This module will be created in a moment. What I don't like about this setup, however, is that the port number is hard-coded, which is not really convenient. I might want to tweak it when starting the application, so let's instead store it in the environment:
Plug.Adapters.Cowboy.child_spec( :http, Sample.Router, [], [port: Application.fetch_env!(:sample, :port)] )
Now provide the default port value inside the config.exs file:
config :sample, port: 8088
Great!
What about the router? Create a new lib/router.ex file with the following contents:
defmodule Sample.Router do use Plug.Router plug :match plug :dispatch end
Now we need to define a couple of routes to perform addition and fetch the result:
get "/result" do conn |> ok(to_string(Sample.CalcServer.result)) end post "/add" do fetch_number(conn) |> Sample.CalcServer.add conn |> ok end
We are using get
and post
macros to define the /result
and /add
routes. Those macros will set the conn
object for us.
ok
and fetch_number
are private functions defined in the following way:
defp fetch_number(conn) do Plug.Conn.fetch_query_params(conn).params["number"] |> String.to_integer end defp ok(conn, data \\ "OK") do send_resp conn, 200, data end
fetch_query_params/2
returns an object with all the query parameters. We are only interested in the number that the user sends to us. All parameters initially are strings, so we need to convert it to integer.
send_resp/3
sends a response to the client with the provided status code and a body. We won't perform any error-checking here, so the code will always be 200
, meaning everything is okay.
And, this is it! Now you may start the application in any of the ways listed above (for example, by typing iex -S mix
) and use the curl
tool to perform the requests:
curl http://localhost:8088/result # => 0 curl http://localhost:8088/add?number=1 -X POST # => OK curl http://localhost:8088/result # => 1
Conclusion
In this article we have discussed Elixir applications and their purpose. You have learned how to create applications, provide various types of information, and list dependencies inside the mix.exs file. You've also seen how to store the configuration inside the app's environment and learned a couple of ways to start your application. Lastly, we have seen applications in action and created a simple web-based calculator.
Don't forget that the hex.pm website lists many hundreds of third-party applications ready for use in your projects, so be sure to browse the catalog and pick the solution that suits you!
Hopefully, you found this article useful and interesting. I thank you for staying with me and until the next time.
No comments:
Post a Comment