********************
Function definitions
********************
Functions take a list of arguments and evaluate the expression in
their body, producing a return value. ABS functions are always pure.
This means the body of a function can use all pure expressions but no
expressions with side effects.
Functions can be *parametric*, which means that they can take and return
parametric datatypes. This means that a function ``head`` defined over a
parametric list datatype can return the first element of a list, regardless of
its type. Parametric functions are defined like normal functions but have an
additional type parameter section inside angle brackets (``<`` ``>``) after the
function name.
.. table:: Syntax
:align: left
:class: syntax
=================== ===========================
*FunctionDecl* ::= ``def`` *Type* *SimpleIdentifier*
\ [ ``<`` *SimpleTypeIdentifier* { ``,`` *SimpleTypeIdentifier* } ``>`` ]
\ ``(`` [ *Type* *SimpleIdentifier* { ``,`` *Type* *SimpleIdentifier* } ] ``)``
\ ``=`` *PureExp* ``;``
=================== ===========================
Example::
def Rat abs(Rat x) = if x > 0 then x else -x; ①
def Int length(List list) = ②
case list {
Nil => 0
| Cons(_, ls) => 1 + length(ls)
};
def A head(List list) = ③
case list { Cons(x, _) => x };
| ① The `abs` function returns the absolute value of its argument.
| ② This parametric function takes lists with arbitrary values and
returns an Integer.
| ③ This parametric function returns the same type that is contained
in the list. (Note that `head` is a partial function which is not
defined for empty lists.)
.. _sec:partially-defined-functions:
Partial function definitions
============================
For reasons of simplicity and analyzability, ABS does not offer
higher-order functions. On the other hand, many common patterns of
functional programming are extremely useful, for example the
well-known ``map``, ``filter`` and ``fold`` higher-order functions.
For this reason, ABS supports *partial function definitions*.
Partial function definitions are function definitions taking an
additional set of parameters. These additional parameters can be
either names of normal functions, or anonymous functions (see
:ref:`sec:anonymous-functions`). Partial function definitions define a
set of functions which only differ in function applications but share
overall structure. Put another way, partial function definitions
define second-order functions -- functions that take first-order
functions as arguments. Partially defined functions can be used
inside functional code, but cannot be passed as parameters to other
partial functions.
A partially defined function is called the same way as a normal
function, with a separate, non-empty argument list containing the
functional arguments. For recursion inside the body of a partially
defined function, omit the function parameter list.
.. table:: Syntax
:align: left
:class: syntax
========================== ===================
*PartialFunctionDecl* ::= ``def`` *Type* *SimpleIdentifier*
\ [ ``<`` *SimpleTypeIdentifier* { ``,`` *SimpleTypeIdentifier* } ``>`` ]
\ ``(`` *SimpleIdentifier* { ``,`` *SimpleIdentifier* } ``)``
\ ``(`` [ *Type* *SimpleIdentifier* { ``,`` *Type* *SimpleIdentifier* } ] ``)``
\ ``=`` *PureExp* ``;``
========================== ===================
Example::
// Apply a function fn A -> B to a value A
def B apply(fn)(A value) = fn(value);
def Int double(Int x) = x * 2;
{
Int doubled = apply(double)(2); ①
}
| ① ``doubled`` will have the value four.
Example::
def List map(f)(List list) = case list { ①
Nil => Nil
| Cons(x, xs) => Cons(f(x), map(xs)) ②
};
def Int double(Int x) = x * 2;
{
// doubled = [2, 4, 6]
List doubled = map(double)(list[1, 2, 3]);
}
| ① This definition of `map` is contained in the standard library.
| ② Note the recursive call to `map` omits the function parameter
list.
.. note:: For each call of a partial function, a normal function
definition is generated at compile time by replacing the
functional parameters syntactically by the functions passed
in the additional parameter list. This is done before type
checking and after delta and trait flattening -- any type
mismatch and similar errors are caught afterwards during
type checking. If multiple statements call a partially
defined function with the same function-name arguments, only
one expansion is generated.
.. _sec:anonymous-functions:
Anonymous functions
===================
To reduce the need to declare a function with a new function name
explicitly every time a partially defined function is called, ABS uses
anonymous functions. Anonymous functions are only allowed in the
first arguments list calls to partially defined functions.
.. table:: Syntax
:align: left
:class: syntax
======================== =================
*AnonymousFunction* ::= ``(`` [ *Type* *SimpleIdentifier* { ``,`` *Type* *SimpleIdentifier* } ] ``)``
\ ``=>`` *PureExp*
======================== =================
An anonymous function specifies a number of parameters and an expression that
may refer to the declared parameters.
The following example is equivalent to the previous example, but does not
define the ``double`` function explicitly::
{
List list = list[1, 2, 3];
list = map((Int y) => y * 2)(list); ①
}
| ① ``list`` will have the value ``list[2, 4, 6]``
Anonymous functions can refer to variables and fields accessible in
the context of the partial function call::
{
Int factor = 5;
List list = list[1, 2, 3];
list = map((Int y) => y * factor)(list); ①
}
| ① ``list`` will have the value ``list[5, 10, 15]``
.. note:: Anonymous functions are inlined into the expansion of the
partial function definition. Errors caused by wrong typing
are caught after the expansion during the type checking of
core ABS, but the expanded function definition has an
annotation referring to the statement that caused the
expansion, hence error reporting will be accurate wrt. the
original source code.
Built-in functions
==================
Some special "functions" cannot be defined with pure expressions, for
example the function ``println``. The definition of such functions is
done via a special function body written as ``builtin``, with optional
pure expression arguments ``builtin(a, "b", 3)``. Such builtin
functions have to be defined separately in the code generator for each
backend where the model is compiled.
Built-in functions in the standard library include:
- The functions ``sqrt``, ``log``, ``exp`` that work on floating-point
numbers
- Functions that convert between different numerical types:
``truncate``, ``float``, ``rat``, ``floor``, ``ceil``,
``numerator``, ``denominator``
- String functions: ``substr``, ``strlen``, ``toString``, ``println``
- Clock access: ``currentms``, ``ms_since_model_start``
- Functions that return process attributes (see Section
:ref:`sec:process-attributes`)
- The ``random`` function
.. _sec:sqlite:
Embedded SQLite database queries
================================
The Erlang and Java backends can read from a relational database and
convert the result into ABS lists. Currently only `SQLite
`__ databases are supported.
A SQLite database is queried by writing a function with a ``builtin``
body with three or more arguments: a literal ``sqlite3``, the name of
the database file, the query as a SQL string, and zero or more
arguments to the query. The return value is a list of ABS values.
The return type of such a function can be:
- ``List``, when the query returns rows with a single SQLite
``INTEGER`` value;
- ``List``, when the query returns rows with a single SQLite
``INTEGER`` or ``REAL`` value;
- ``List``, when the query returns rows with a single SQLite
``INTEGER`` or ``REAL`` value;
- ``List``, when the query returns rows with a single SQLite
``INTEGER`` value, where ``0`` corresponds to ABS ``False``;
- ``List``, when the query returns rows with a single SQLite
``TEXT`` value;
- A list of an algebraic datatype, with a single constructor taking
only the above datatypes, such that the constructor can be invoked
for each row returned by the query.
Query parameters are written as ``?`` in the SQL query string. Each
of these parameters must be supplied with a value. ABS query
parameters are converted into SQL values (see
https://www.sqlite.org/datatype3.html) as follows:
- ``Int`` values are converted into ``INTEGER``;
- ``Float`` values are converted into ``REAL``;
- ``Rat`` values are converted into floating-point numbers before
passing them to the query function;
- ``Bool`` values are converted into ``0`` and ``1`` (note that SQLite
treats all non-zero values as true; consider directly using a query
parameter of type ``Int`` instead);
- ``String`` values are converted into ``STRING``.
Example: creating a database from the command line::
$ sqlite3 /tmp/test.sqlite3
CREATE TABLE IF NOT EXISTS test_table (
int_value INTEGER,
float_value REAL,
string_value TEXT,
bool_value BOOLEAN
);
INSERT INTO test_table(int_value, float_value, string_value, bool_value)
VALUES (15, 13.53, "hello", 0);
INSERT INTO test_table(int_value, float_value, string_value, bool_value)
VALUES (30, 42.5, "world", 1);
.quit
With the above database, the following ABS model can be run:
Example: reading from the database::
module Test;
def List fstring() = builtin(sqlite3, ①
"/tmp/test.sqlite3",
"SELECT string_value FROM test_table");
def List frat() = builtin(sqlite3, ②
"/tmp/test.sqlite3",
"SELECT float_value FROM test_table");
data RowResult = RowResult(Int, Bool, Float, Rat, String);
def List ftuple() = builtin(sqlite3, ③
"/tmp/test.sqlite3",
`SELECT int_value, bool_value, float_value, float_value, string_value
FROM test_table`);
def List ftuple_with_params(String str, Rat rat) = builtin(sqlite3,
"/tmp/test.sqlite3",
`SELECT int_value, bool_value, float_value, float_value, string_value
FROM test_table
WHERE string_value = ? ④
AND float_value = ?`,
str, ⑤
rat);
{
foreach (v, i in frat()) {
println(`$i$'th rational value is $v$`);
}
foreach (v, i in fstring()) {
println(`$i$'th string value is $v$`);
}
foreach (v, i in ftuple()) {
println(`$i$'th tuple is $v$`);
}
foreach (v, i in ftuple_with_params("world", 85/2)) {
println(`$i$'th tuple is $v$`);
}
}
| ① A `builtin` function with first argument `sqlite3` takes two or
more additional arguments: the name of the database, a SQL query
string, and the query parameters.
| ② SQL `REAL` values are converted to ABS rational values
| ③ SQL query results with more than one column are converted into ABS
user-defined datatypes. The data constructor of the result datatype
has to take the same number of arguments as the `SELECT` returns,
and the datatypes must be compatible.
| ④ Query parameters in SQLite are written as plain `?` in the query
string.
| ⑤ For each parameter in the SQL query, a value must be supplied.
For the Erlang backend, when the database file (the second argument to
the ``builtin`` expression) does not contain a path, the model will
look for it in the ``priv`` directory of the compiled model. That
directory is the value of the erlang function
``code:priv_dir(absmodel)``, typically
``gen/erl/absmodel/_build/default/lib/absmodel/priv/``. For the Java
backend, the path to the database file is resolved relative to the
current path of the process running the model.