Erlang for the Little Ones. Modules and Functions
ErlangDear %username%, Let’s continue to learn Erlang.
In the previous post we’ve reviewed the basic data types, lists and tuples. We also learnt how to use pattern matching and lists generator. In this post we’ll move on to the next level and review modules and functions.
All functions in Erlang are defined in modules. Standard functions of the language, which we invoke as “global” (for instance, length, hd, tl), are actually inside of the module as well. They are BIFs (Built-In Functions) and belong to erlang module. This module is imported by default. Therefore, we can work with them like with regular functions (I’ll tell you more about the module importing below).
Modules
Modules are a group of logically connected functions, grouped by one name. Put it crudely, modules in Erlang are the analog of the name space in imperative languages. We use them to unite functions having similar value. For example, functions for operating with lists are located in lists module, while input/output functions are in io module.
To call a function we should use the following construction: ModuleName:FunctionName(Arg1, Arg2, …, ArgN). As an example, let’s call a function returning the element of the passed tuple with the indicated number. This function is called element and located in erlang module.
1> erlang:element(3, {23,54,34,95}).
34
There’s also an ability to call functions without an explicit module indication. More about it later. Modules contain functions and attributes.
Module Attributes
As well as in imperative languages, attributes are not variables. In Erlang, module attributes are its metadata, such as name, version, author, list of imported functions, etc. The compiler uses attributes. We can also get useful information about a module from them without examining the source code (for example, the version and the author).
We indicate attributes at the very beginning of a file. They look like the following: -Name(Arg).. The module name should be an atom. Each attribute goes on a separate line.
You can assign attribute to a module. For example, to describe the mood you had when creating it. There’s also a series of predefined attributes. Let’s take a look at the ones most commonly used. Let’s create a module to illustrate them. It will contain functions performing the simplest math operations: addition, subtraction, multiplication and division.
-module(Name). Module name is the only mandatory attribute. You should always declare it in the first place. Your module won’t compile without it. It accepts an atom – the module name – as an argument. Let’s name our module mySuperModule.
-module(mySuperModule).
Now we have quite an operable module. We’ve declared the only mandatory attribute. So now we can compile our module. It’s absolutely useless as there are no functions in it. But practically it’s a ready module.
-export([Fnct1/Arity, Fnct2/Arity, …, FnctN/Arity]) The list of exported functions is the list of the module functions that will be available from outside. The attribute it accepts is the list of functions. Fnct here is the function name and Arity is the number of arguments accepted by it. Our module will export four functions: add, subtr, mult, divis (addition, subtraction, multiplication and division). Each function will accept two arguments.
-export([add/2, subtr/2, mult/2, divis/2]).
You should keep in mind that you will not be able to call the functions from outside if they are not in the export list. In this case you’ll able to work with them inside of the module only. Export is the means of achieving encapsulation in the module. As you might have guessed, exported functions are the analog of public methods in imperative languages. The rest of the functions are the analogue of the private ones.
-import(ModuleName, [Fnct1/Arite, Fnct2/Arity, …, FnckN/Arity]). This attribute indicates that we want to import from ModuleName module the functions we declared in the list, passed as the second argument. Every imported module should be specified in a separate attribute.
Why to import functions? As I’ve mentioned before, in order to refer to the function from another module we should specify its full name, as follows: ModuleName:FunctionName(). If you don’t want to write the module name every time, you should import it. This attribute is similar to #using directive from C++. But we shouldn’t misuse importing. The full name of the function is more demonstrable. Having seen it we can say at once, which module the called function belongs to. If the name is short we’ll have to remember, which module this function was imported from.
We’re going to use full names of the functions. But if we wanted to use short names, we could write something like that:
-import(io, [format/2]).
Well, let’s also specify some random attribute. Let it be the author name.
-author("Satoshi Nakamoto").
You can examine the full list of predefined attributes in the Erlang official documentation.
If we try to compile our module now, we’ll get an error:
1> c(mySuperModule).
./mySuperModule.erl:2: function add/2 undefined
./mySuperModule.erl:2: function divis/2 undefined
./mySuperModule.erl:2: function mult/2 undefined
./mySuperModule.erl:2: function subtr/2 undefined
As you can see from the text of the error, the compiler can’t find the functions we’ve declared in the import list. It’s is quite logical, since we haven’t added them yet. Let’s fix this error by creating the functions.
Functions
In the base case, functions in Erlang look like the following: FnctName(Arg1, Arg2, …, ArgN) -> FunctionBody. The function name is an atom. The function body is one or several expressions divided by commas. We put a period in the end of the function body. If the function contains just one expression, it’d be better to write it in one line:
add(X, Y) -> X + Y.
Our function accepts two arguments and returns their sum. We should pay attention to the absence of return word. The thing is that in Erlang a function always returns the result of the last expression. In our case it’s the result of addition. Therefore, we just don’t need return word.
But a function doesn’t always consist of one expression. In this case, we’ll highlight the function body by indenting on the left from the remaining code. So our function will look like the following:
add(X, Y) ->
doSomthing(),
X + Y.
Now add the remaining three functions on your own. In order to try out everything we have created, let’s compile our module.
Compilation
The programs written in Erlang are compiled into an intermediate byte-code. It will be executed later on the virtual machine. As a result of it, applications written in Erlang are cross-platform. There are few more virtual machines for Erlang. But the most popular is BEAM(Bogdan/Björn’s Erlang Abstract Machine). There’re also other virtual machines (JAM and WAM), but they are not in use. Therefore, we won’t consider them.
There are two ways of compiling: from the terminal and the command line of Erlang. Let’s take a look at both variants.
In order to compile from the terminal we should open the directory with the file and run erlc FileName.erl command. It will look like the following for our module (you may have your own):
$ cd ~/Erlang-for-the-little-ones/02/sources
$ erlc mySuperModule.erl
In order to do it from the Erlang command line we should also browse to the directory by calling cd(«DirName») command. Then we should call c(ModuleName) command. Please note, that we pass the module name, not the file name. There’s no need to specify the extension.
$ erl
1> cd("~/Erlang-for-the-little-ones/02/sources/").
/home/haru/Erlang-for-the-little-ones/02/sources
ok
2> c(mySuperModule).
{ok,mySuperModule}
As a result of compilation mySuperModule.beam file will occur next to mySuperModule.erl file. That’s the compiled module. Now we can use it. Let’s try:
1> mySuperModule:add(2, 4).
6
2> mySuperModule:divis(6,4).
1.5
It’s worth noting that there’s an ability of passing “compilation flags” to the compiler. For that purpose we should pass the second argument to c() function. As an example, let’s compile our module in the debug mode:
c(mySuperModule, [debug_info]).
We are not going to focus on that right now. A separate article will cover this subject. But if you’re interested, you can take a look at the list of keys in documentation.
Summary
In this article we got familiar with modules and functions. We’ve also found out how to compile our code so that we could use it. In the next article we’ll review the syntax of functions in details. We’ll also talk about pattern matching.
Thank you for reading the post. Have a nice day and keep coding!
Comments