Creating a simple escript with Erlang
I am currently toying with Erlang a bit. I always loved the language
for its pragmatism and its philosophy of software design.
I first learned Erlang after learning Haskell, which means that
functional programming was not weird to me anymore. With Erlang being
impure and dynamically typed, you loose some superpowers you get with
Haskell - and I sometimes miss the expressivity you get with lifting
functions across a stack of types. But on the other hand, Erlang is
more pragmatic: You write code that looks simple and Erlang encourages
you to keep writing code that way.
The true superpowers of Erlang cannot be found in monads, but in the
architecture of OTP (formerly an abbreviation for Open Telecom
Platform, nowadays without a special meaning): By splitting your
application into different processes (BEAM processes, not OS
processes) and reusing so-called "behaviors" to design and implement
these processes, you quickly get a modular architecture that is
potentially so much more maintainable than other software. Disclaimer:
Of course, you can write horrible code in every language.
In my eyes, the most important behavior is the supervisor, which is
the central point in error handling and therefore fault tolerance. A
supervisor monitors other processes and restarts them on crashes,
restarting other dependent processes as necessary. If the supervisor
itself cannot handle the problem (i.e. the crashes keep happening), it
crashes itself and escalates the problem to its supervisor. By
splitting responsibilities, every process itself can be kept simple
and therefore will have less errors than it would have otherwise.
The deploying story
One negative aspect of BEAM languages like Erlang always is that you
cannot just compile to a single binary. Erlang falls into some kind of
uncanny valley between compiled languages like C++, Rust or Haskell on
one side, and interpreted languages like Python on the other side:
Erlang is compiled, so you normally would not simply copy some script
to your target - instead, you have to compile your scripts first and
copy the output files. But then, just copying these files will not
help you without an Erlang installation to execute them.
This situation is comparable to Java, where you also have to compile
your source into class files (that usually are bundled inside of an
archive), but unlike Java, Erlang is much more rarely installed and if
you want to deploy software written in Erlang, you are responsible for
also deploying Erlang yourself. My former team developed software in
Java (mostly), so I could have shared jar files with everyone in my
team without any complains, but if I were to give them my Erlang
programs, they would not know anything about it.
To solve this, Erlang has releases that can bundle your compiled
program with the BEAM virtual machine and the necessary standard
libraries. You will get a folder instead of a single executable file,
but you can copy it to other computers using the same operating system
and hardware architecture and things will just work. This is also part
of the toolkit provided by OTP. Build tools like rebar3 make creating
releases manageable. This is the way for doing things in a serious
environment.
For my own use, I also would like to just have a simple binary in some
place that I can execute without much ceremony. Since we are talking
about my own systems here, I can install Erlang once centrally, so it
would be a waste to create a full release for each binary. Enter
escripts: An escript still needs a BEAM virtual machine somewhere in
the path, but otherwise is a self-contained archive of all modules
(your own and your dependencies) that can simply be executed. It is
therefore very comparable to Javas jar files.
In this article, I will now explain how to create one, using a simple
example. I like writing number guessing games in different languages
to familiarize myself with the syntax and standard library, so let's
do this here!
Setup
We will need an Erlang environment, consisting of both the Erlang
compiler and shell as well as the BEAM virtual machine. We will also
want to use the rebar3 build tool - you can imagine it to be the
Erlang equivalent to cargo.
For Debian, you can install these with:
apt install erlang rebar3
For OpenBSD 7.5-Current, I would install
pkg_add -Dsnap -i erlang erl26-rebar3
I will give the rebar3 commands as if the binary was just named
"rebar3", but on my OpenBSD system, I am using "rebar3-26"
instead. This should not matter much at all.
Creating the project
We first have to create a new project using rebar3. This is as simple
as going to your desired parent folder and executing
rebar3 new escript guess_number
This creates a new folder "guess_number" containing a few files to get
your project started. You should see an output similar to this:
===> Writing guess_number/src/guess_number.erl ===> Writing guess_number/src/guess_number.app.src ===> Writing guess_number/rebar.config ===> Writing guess_number/.gitignore ===> Writing guess_number/LICENSE.md ===> Writing guess_number/README.md
Your main code will go into the guess_number.erl, guess_number.app.src
contains the definition of the guess_number OTP application, which you
will not need to change here.
Improving escript configuration
From inside the project folder, you can now build a executable escript
that will call the guess_number:main/1 function. To do this, execute:
rebar3 escriptize
Your escript is then stored as ._build/default/bin/guess_number. If
you execute it, it will print the argument list given to it back to
you.
Using the default configuration, the escript determines what module to
call by looking at its own file name. This means that if you copy your
escript to my_number_guessing_game, it will stop functioning. This can
be useful if you want to implement something similar to the busybox
shell binaries, where links with different names all point to the same
binary file.
If you do not want that, you can change your rebar.config to point to
a fixed module:
{erl_opts, [no_debug_info]}.
{deps, []}.
{escript_incl_apps,
[guess_number]}.
{escript_main_app, guess_number}.
{escript_name, guess_number}.
{escript_emu_args, "%%! +sbtu +A1 -escript main guess_number\n"}.
%% Profiles
{profiles, [{test,
[{erl_opts, [debug_info]}
]}]}.
This file is mostly unchanged from the default, but I extended
escript_emu_args to contain "-escript main guess_number".
For the curious, the default arguments given here are "+sbtu", which
I understand to tell the Erlang scheduler to freely use all CPU cores
without pinning specific execution threads to specific cores, and
"+A1", which reserves one asynchronous thread that will be used for
stuff like IO or calling native code.
I leave the default arguments as they are since I neither have enough
experience to know it better, nor have I a need for specific
optimizations.
With my addition, you can call the escript whatever you want and it
will execute the guess_number module.
Implementing the game
Without further ado, here is the code I have written in the
guess_number.erl (and joining the template code generated for me):
-module(guess_number). %% API exports -export([main/1]). %%==================================================================== %% API functions %%==================================================================== %% escript Entry point main(_Args) -> loop(), erlang:halt(0). %%==================================================================== %% Internal functions %%==================================================================== loop() -> io:format("Number guessing game - guess a number between 1 and 100!~n"), Target = rand:uniform(100), game(Target), case ask_try_again() of true -> loop(); false -> ok end. game(Target) -> Guess = get_guess(), if Target == Guess -> io:format("Correct!~n"), ok; Target > Guess -> io:format("Too small!~n"), game(Target); true -> io:format("Too big!~n"), game(Target) end. get_guess() -> Guess = io:get_line("Guess a number: "), GuessParsed = string:to_integer(string:trim(Guess)), case GuessParsed of {GuessInt, ""} when GuessInt >= 1 andalso GuessInt =< 100 -> GuessInt; _ -> get_guess() end. ask_try_again() -> Answer = io:get_line("Play again? [y/n] "), case string:trim(Answer) of "y" -> true; "Y" -> true; "n" -> false; "N" -> false; _ -> ask_try_again() end.
The code itself should be straight-forward if you already understand
enough Erlang to read it.
As a refresher: Names starting with a capital letter are variables,
lowercase names are atoms (comparable with symbols in languages like
Lisp or Julia) that are either used as values (e.g. "true", "false",
"ok") or naming functions (local like "ask_try_again" or as a
module-function pair like "io:get_line").
As a primary functional language, Erlang uses pattern matching when
using constructs like "case" or even just using the "=" operator,
which is a matching operator and no assignment operator (Erlang
disallows reassignment of variables to different values, but you can
match them multiple times).
Erlang also does not contain any loop constructs in the language. This
is just like in Haskell: You either use recursion, which profits from
tail call optimization and therefore will run in static space, or you
can use higher order functions like lists:map, which I have not needed
here, but could be used like this:
1> lists:map(fun(X) -> X * 2 end, [1,2,3]). [2,4,6]
Conclusion
This article should have given you enough information to write and
compile a simple escript. This example does not really highlight the
strengths and advantages of Erlang. I have chosen a simple example,
which might not blow your mind, but expands on a "hello world" program
as generated by rebar3.