Even if this document does not constitute an exhaustive walk-through, each of them is detailed in turn below.
The next level of detail is to peer at the referenced source files, as they include many implementation notes, comments and typing information.
It may be useful to decide, at compile-time, whether some code should be added / removed / transformed / generated based on tokens defined by the user.
This is done here thanks to the use of conditional primitives and associated compilation defines (sometimes designated as "macros", and typically specified in makefiles, with the -D flag).
These conditional primitives are gathered in the cond_utils module.
As an early example, so that a piece of code prints Hello! on the console when executed iff (if and only if) the my_token compilation token has been defined (through the -Dmy_token command-line flag), one may use:
cond_utils:if_defined(my_token, io:format("Hello!"))
Of course, as such a code injection is done at compilation-time, should compilation defines be modified the modules making use of the corresponding primitives shall be recompiled so that these changes are taken into account.
Let's enter a bit more in the details now.
A token (a compilation-time symbol) may or may not defined.
To define my_token, simply ensure that a -Dmy_token command-line option is specified to the compiler (ex: refer to ERLANG_COMPILER_TOKEN_OPT, in GNUmakevars.inc, for an example of definition for these flags).
To define my_token and set it to the integer value 127, use the -Dmy_token=127 command-line option. Values can also be floats (ex: -Dmy_token=3.14) or atoms (ex: -Dmy_token=some_atom).
A special token is myriad_debug_mode; if it is defined at all (and possibly associated to any value), the debug mode of Myriad is enabled.
We recommend that layers built on top of Myriad define their own token for debug mode (ex: foobar_debug_mode), to be able to finely select appropriate debug modes (of course all kinds of modes and configuration settings can be considered as well).
Based on the defined tokens, code may be injected; this code can be any Erlang expression, and the value to which it will evaluate (at runtime) can be used as any other value in the program.
Injecting a single expression (i.e. not multiple ones) is not a limitation: not only this single expression can be a function call (thus corresponding to arbitrarily many expressions), but more significantly a series of expressions can be nested in a begin / end block, making them a single expression .
Various primitives for code injection are available in the cond_utils (mostly pseudo-) module .
There is first if_debug/1, to be used as:
cond_utils:if_debug(EXPR_IF_IN_DEBUG_MODE)
Like in:
A = "Joe",
cond_utils:if_debug(io:format("Hello ~s!",[A]))
or, to illustrate expression blocks:
cond_utils:if_debug(begin
C=B+1,
io:format("Goodbye ~p",[C])
end)
These constructs will be replaced by the expression they specify for injection, at their location in the program, iff the myriad_debug_mode token has been defined, otherwise they will be replaced by nothing at all (hence with exactly no runtime penalty; and the result of the evaluation of if_debug/1 is then not an expression).
Similarly, if_defined/2, used as:
cond_utils:if_defined(TOKEN, EXPR_IF_DEFINED)
will inject EXPR_IF_DEFINED if TOKEN has been defined (regardless of any value associated to this token), otherwise the if_defined/2 call will be removed as a whole .
As for if_defined/3, it supports two expressions:
cond_utils:if_defined(TOKEN, EXPR_IF_DEFINED, EXPR_OTHERWISE)
For example:
% Older versions being less secure:
TLSSupportedVersions = cond_utils:if_defined(us_web_relaxed_security,
['tlsv1.3', 'tlsv1.2', 'tlsv1.1', 'tlsv1'],
['tlsv1.3'])
If us_web_relaxed_security has been defined, the first list will be injected, otherwise the second will.
Note that a call to if_defined/3 results thus in an expression.
Finally, with if_set_to/{3,4}, the injection will depend not only of a token being defined or not, but also onto the value (if any) to which it is set.
For if_set_to/3:
cond_utils:if_defined(TOKEN, VALUE, EXPR_IF_SET_TO_THIS_VALUE)
will inject EXPR_IF_SET_TO_THIS_VALUE iff TOKEN has been defined and set to VALUE. As a result, the specified expression will not be injected if some_token has been set to another value, or not been defined at all.
Usage example, -Dsome_token=42 having possibly been defined beforehand:
cond_utils:if_set_to(some_token,42, SomePid ! hello)])
As for if_set_to/4, in:
cond_utils:if_set_to(TOKEN, VALUE, EXPR_IF_SET_TO_THIS_VALUE, EXPR_OTHERWISE)
EXPR_IF_SET_TO_THIS_VALUE will be injected iff TOKEN has been defined and set to VALUE, otherwise (not set or set to a different value) EXPR_OTHERWISE will be.
Example:
Level = cond_utils:if_set_to(my_token, foobar_enabled, 1.0, 0.0) + 4.5
Finally, the switch_set_to/{2,3} primitives allow to generalise these if-like constructs, with one among any number of code branches selected based on the build-time value of a token, possibly with defaults (should the token not be defined at all, or defined to a value that is not among the ones associated to a code branch).
For that we specify a list of pairs, each made of a value and of the corresponding expression to be injected if the actual token matches that value, like in:
cond_utils:switch_set_to(TOKEN, [
{VALUE_1, EXPR_1},
{VALUE_2, EXPR_2},
% [...]
{VALUE_N, EXPR_N}])
For example:
cond_utils:switch_set_to(my_token, [
{my_first_value, io:format("Hello!")},
{my_second_value, begin f(), g(X,debug), h() end},
{some_third_value, a(X,Y)}])
A compilation-time error will be raised if my_token is not set, or if it is set to none of the declared values (i.e. not in [my_first_value, my_second_value, some_third_value]).
A variation of this primitive exists that applies a default token value if none was, or if the token was set to a value that is not listed among any of the ones designating a code branch, like in:
cond_utils:switch_set_to(TOKEN,
[ {VALUE_1, EXPR_1},
{VALUE_2, EXPR_2},
% [...]
{VALUE_N, EXPR_N}],
DEFAULT_VALUE)
As always with primitives that define a default, alternate branch, they always inject an expression and thus can be considered as such.
For example:
ModuleFilename = atom_to_list( cond_utils:switch_set_to(some_token,
[{1, foo}, {14, bar}, {20, hello}], 14) ++ ".erl"
Here, if some_token is not defined, or defined to a value that is neither 1, 14 or 20, then the 14 default value applies, and thus ModuleFilename is set to "bar.erl".
Refer to cond_utils_test.erl for further usage examples.
It may be convenient that, depending on a compile-time token (ex: in debug mode, typically triggered thanks to the -Dmyriad_debug_mode compilation flag), assertions (expressions expected to evaluate to the atom true) are enabled, whereas they shall be dismissed as a whole should that token not be defined.
To define an assertion enabled in debug mode, use assert/1, like in:
cond_utils:assert(foo(A,B)=:=10)
Should at runtime the expression specified to assert/1 be evaluated to a value V that is different from the atom true, a {assertion_failed,V} exception will be thrown.
More generally, an assertion may be enabled by any token (not only myriad_debug_mode) being defined, like in:
cond_utils:assert(my_token,bar(C))
Finally, an assertion may be enabled iff a token (here, some_token) has been defined and set to a given value (here, 42), like in:
cond_utils:assert(some_token,42,not baz() andalso A)
This may be useful for example to control, on a per-theme basis, the level of checking performed, like in:
cond_utils:assert(debug_gui,1,basic_testing()),
cond_utils:assert(debug_gui,2,more_involved_testing()),
cond_utils:assert(debug_gui,3,paranoid_testing()),
Note that, in this case, a given level of checking should include the one just below it (ex: more_involved_testing() should call basic_testing()).
For tokens, at least currently they must be defined as immediate values (atoms); even using a mute variable, like for the _Default=my_token expression, or a variable, is not supported (at least yet).
Note that, for primitives that may not inject code at all (ex: if_debug/1), if their conditions are not fulfilled, the specified conditional code is dismissed as a whole, it is not even replaced for example by an ok atom; this may matter if this conditional is the only expression in a case clause for example, in which case a compilation failure like "internal error in core; crash reason: function_clause in function v3_core:cexprs/3 called as v3_core:cexprs[...]" will be reported (the compiler sees unexpectedly a clause not having even a single expression).
A related issue may happen when switching conditional flags: it will select/deselect in-code expressions at compile time, and may lead functions and/or variables to become unused, and thus may trigger at least warnings .
For functions that could become unused due to the conditional setting of a token, the compiler could certainly be silenced by exporting them; yet a better approach is surely to use:
-compile({nowarn_unused_function,my_func/3}).
or:
-compile({nowarn_unused_function,[my_func/3, my_other_func/0]}).
As for variables, should A, B or C be reported as unused if some_token was not set, then the basic_utils:ignore_unused/1 function (mostly a no-op) could be of use:
[...]
cond_utils:if_defined(some_token,
f(A, B, C),
basic_utils:ignore_unused([A, B, C])),
[...]
Alternatively, nowarn_unused_vars could be used instead, at least in some modules.
Some services have been defined, in myriad/src/user-interface, in order to handle more easily interactions with the user, i.e. to provide a user interface.
Note
The user-interface services, as a whole, are currently not functional. A rewriting thereof as been started yet has not completed yet.
Such a user interface may be:
- either text-only, within a console, relying either on the very basic text_ui (for raw text) or its more advanced term_ui counterpart (for terminal-based outputs)
- or graphical, with gui
Text-based user interfaces are quite useful, as they are lightweight, incur few dependencies (if any), and can be used with headless remote servers (text_ui and term_ui work well through SSH, and require no X server nor mouse).
As for graphical-based user interfaces, they are the richest, most usual, and generally the most convenient, user-friendly interfaces.
The user interfaces provided by Myriad are stateful, they rely on a state that can be:
- either explicit, in a functional way; thus having to be carried in all calls
- or implicit, using - for that very specific need only - the process dictionary (even if we try to stay away of it as much as possible)
We tested the two approaches and preferred the latter (implicit) one, which was found considerably more flexible and thus finally fully superseded the (former) explicit one.
We made our best so that a lower-level API interface (relying on a more basic backend) is strictly included in the higher-level ones (ex: term_ui adds concepts - like the one of window or box - to the line-based text_ui), in order that any program using a given user interface may use any of the next, upper ones as well (provided implicit states are used), in the following order: the text_ui API is included in the one of term_ui, which is itself included in the one of gui.
We also defined the settings table, which is a table gathering all the settings specified by the developer, which the current user interface does its best to accommodate.
Thanks to these "Matryoshka" APIs and the settings table, the definition of a more generic ui interface has been possible. It selects automatically, based on available local software dependencies, the most advanced available backend, with the most relevant settings.
For example a relevant backend will be automatically selected by:
$ cd test/user-interface/src
$ make ui_run
On the other hand, if wanting to select a specified backend:
$ make ui_run CMD_LINE_OPT="--use-ui-backend term_ui"
(see the corresponding GNUmakefile for more information)
This is the most basic, line-based monochrome textual interface, directly in raw text with no cursor control.
Located in {src,test}/user-interface/textual, see text_ui.erl for its implementation, and text_ui_test.erl for an example of its use.
This is a more advanced textual interface than the previous one, with colors, dialog boxes, support of locales, etc., based on dialog (possibly whiptail could be supported as well). Such backend of course must be available on the execution host then.
For example, to secure these prerequisites:
# On Arch Linux:
$ pacman -S dialog
# On Debian-like distros:
$ apt-get install dialog
Located in {src,test}/user-interface/textual, see term_ui.erl for its implementation, and term_ui_test.erl for an example of its use.
This interface relied initially on gs (now deprecated), now on wx (a port of wxWidgets), maybe later in HTML 5 (possibly relying on the Nitrogen web framework for that). For the base dialogs, Zenity could have been on option.
Note
GUI services are currently being reworked, to provide a gs-like concurrent API while relying underneath on wx, with some additions (such as canvases).
The goal is to provide a small, lightweight API (including message types) that are higher-level than wx, and do not depend on any particular GUI backend (such as wx, gs, etc.) to avoid that user programs become obsolete too quickly, as backends for GUI rise and fall relatively often.
So for example the messages received by the user programs shall not mention wx, and they should take a form compliant with WOOPER message conventions, to easily enable user code to rely on WOOPER if wanted.
Located in {src,test}/user-interface/graphical, see gui.erl, gui_color.erl, gui_text.erl, gui_canvas.erl, etc., with a few tests (gui_test.erl, lorenz_test.erl).
Related information of interest:
Some amount of SQL (Structured Query Language) support for relational database operations is provided by the Myriad layer.
As this support relies on an optional prerequisite, this service is disabled by default.
To perform SQL operations, a corresponding software solution must be available.
The SQL back-end chosen here is the SQLite 3 library. It provides a self-contained, serverless, zero-configuration, transactional SQL database. It is an embedded SQL database engine, as opposed to server-based ones, like PostgreSQL or MariaDB.
It can be installed on Debian thanks to the sqlite3 and sqlite3-dev packages, sqlite on Arch Linux..
We require version 3.6.1 or higher (preferably: latest stable one). It can be checked thanks to sqlite3 --version.
Various related tools are very convenient in order to interact with a SQLite database, including sqlitebrowser and sqliteman.
On Arch Linux, one can thus use: pacman -Sy sqlite sqlitebrowser sqliteman.
Testing the back-end:
$ sqlite3 my_test
SQLite version 3.13.0 2016-05-18 10:57:30
Enter ".help" for usage hints.
sqlite> create table tblone(one varchar(10), two smallint);
sqlite> insert into tblone values('helloworld',20);
sqlite> insert into tblone values('my_myriad', 30);
sqlite> select * from tblone;
helloworld|20
my_myriad|30
sqlite> .quit
A file my_test, identified as SQLite 3.x database, must have been created, and can be safely removed.
This database system is directly accessed thanks to an Erlang binding.
Two of them have been identified as good candidates:
- erlang-sqlite3: seems popular, with many contributors and users, actively maintained, based on a gen_server interacting with a C-node, involving only a few source files
- esqlite: based on a NIF, so more able to jeopardize the stability of the VM, yet potentially more efficient
Both are free software.
We finally preferred erlang-sqlite3.
By default we consider that this back-end has been installed in ~/Software/erlang-sqlite3. The SQLITE3_BASE variable in myriad/GNUmakevars.inc can be set to match any other install path.
Recommended installation process:
$ mkdir ~/Software
$ cd ~/Software
$ git clone https://github.com/alexeyr/erlang-sqlite3.git
Cloning into 'erlang-sqlite3'...
remote: Counting objects: 1786, done.
remote: Total 1786 (delta 0), reused 0 (delta 0), pack-reused 1786
Receiving objects: 100% (1786/1786), 3.24 MiB | 570.00 KiB/s, done.
Resolving deltas: 100% (865/865), done.
Checking connectivity... done.
$ cd erlang-sqlite3/
$ make
rm -rf deps ebin priv/*.so doc/* .eunit/* c_src/*.o config.tmp
rm -f config.tmp
echo "normal" > config.tmp
./rebar get-deps compile
==> erlang-sqlite3 (get-deps)
==> erlang-sqlite3 (compile)
Compiled src/sqlite3_lib.erl
Compiled src/sqlite3.erl
Compiling c_src/sqlite3_drv.c
[...]
Testing the binding:
make test
./rebar get-deps compile eunit
==> erlang-sqlite3 (get-deps)
==> erlang-sqlite3 (compile)
==> erlang-sqlite3 (eunit)
Compiled src/sqlite3.erl
Compiled src/sqlite3_lib.erl
Compiled test/sqlite3_test.erl
======================== EUnit ========================
module 'sqlite3_test'
sqlite3_test: all_test_ (basic_functionality)...[0.002 s] ok
sqlite3_test: all_test_ (table_info)...ok
[...]
sqlite3_lib: delete_sql_test...ok
sqlite3_lib: drop_table_sql_test...ok
[done in 0.024 s]
module 'sqlite3'
=======================================================
All 30 tests passed.
Cover analysis: ~/Software/erlang-sqlite3/.eunit/index.html
Pretty reassuring.
To enable this support, once the corresponding back-end (see Database Back-end) and binding (see Erlang SQL Binding) have been installed, the USE_SQLITE variable should be set to true in myriad/GNUmakevars.inc and Myriad shall be rebuilt.
Then the corresponding implementation (sql_support.erl) and test (sql_support_test.erl), both in myriad/src/data-management, will be built (use make clean all from the root of Myriad) and able to be run (execute make sql_support_run for that).
Testing it:
$ cd myriad/src/data-management
$ make sql_support_run
Compiling module sql_support.erl
Compiling module sql_support_test.erl
Running unitary test sql_support_run
[...]
--> Testing module sql_support_test.
Starting SQL support (based on SQLite3).
[...]
Closing database.
Stopping SQL support.
--> Successful end of test.
(test finished, interpreter halted)
Looks good.
The purpose here is to ensure a sufficient code homogeneity; for example in all source files are in such a "canonical form", analysing their differences (diff) is made simpler.
Any text editor can be used, provided that it saves source files with the UNIX, not DOS, conventions (i.e. lines terminating by the LF character, not by the CRLF characters).
The use of syntax highlighting is encouraged.
Recommended text editors are:
- emacs / xemacs
- nedit
- ErlIDE (based on Eclipse)
- gedit
Source files should be formatted for a 80-character width: no character should be present after the 79th column of a line.
Except in very specific cases, only ASCII code should be used (ex: no accentuated characters).
Tabulations should be preferred to series of spaces, and the text should be formatted according to 4-character tabulations.
All redundant whitespaces should be removed, preferably automatically (see the Emacs whitespace-cleanup command). This is why, with the emacs settings that we recommend, pressing the F8 key removes for example the yellow areas in the current buffer by replacing any series of four spaces by a corresponding tabulation.
We would prefer that contributed files (especially source ones) are "whitespace-clean" before being committed. As mentioned, such a transformation can be done directly from Emacs. If using another editor, please ensure that the fix-whitespaces.sh script has been run on the target sources (possibly automatically thanks to a VCS hook) before committing them.
All elements of documentation should be written in English, possibly translated to other languages. Spell-checking is recommended.
In terms of coding style, we would like that the sources remain as uniform as possible, regarding naming, spacing, code/comments/blank line ratios.
We would like that, roughly and on average, the same ratio applies for blank lines, comments and lines of code.
For that one may use the either directly make stats from the root of the layer or the make-code-stats.sh script.
For example:
In the Erlang source code found from XXX/ceylan/myriad, we have:
+ 160 source files (*.erl), 54 header files (*.hrl)
+ a grand total of 89151 lines:
- 27186 of which (30.4%) are blank lines
- 28751 of which (32.2%) are comments
- 33214 of which (37.2%) are code
As not all typos may be detected at compilation-time (ex: wrong spelling for a module), we recommend, for source code, the use of additional static checkers, as discussed in the type-checking section.
Two execution target modes have been defined:
- development (the default): meant to simplify the task of developers and maintainers by reporting as much information and context as possible, even at the expense of some performances and reliability (ex: no retry in case of failure, shorter time-outs not to wait too long in case of errors, more checks, etc.)
- production: mostly the reciprocal of the development mode, whose purpose is to favor efficient, bullet-proof operations
These execution targets are compile-time modes, i.e. they are set once for all when building the layer at hand (probably based, if using OTP, on the rebar corresponding modes - respectively dev and prod).
See EXECUTION_TARGET in GNUmakevars.inc to read and/or set them.
The current execution target is of course available at runtime on a per-layer level, see basic_utils:get_execution_target/0 for more information.
This function shall be compiled once per layer to be accurate, in one of its modules. It is just a matter of adding the following include in such module:
-include_lib("myriad/utils/basic_utils.hrl").
- for clarity, we tend to use longer variable names, in CamelCase
- we tend to use mute variables to clarify meanings and intents, as in _Acc=[] (beware, despite being muted, any variable in scope that bears the same name will be matched)
- as there is much list-based recursion, a variable named H means Head and T means Tail (as in [Head|Tail])