Index RSS

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.