Table of Contents
Last update: 29.01.2025.
In Wafl, programs are expressions. Each program (or expression) primarily defines what is to be evaluated and not how it is to be executed or evaluated.
If we want to write a program that computes 1+2+3, then we only write that expression and nothing else:
1+2+3
6
We can use literals, names, operators and functions in expressions. Here are a few simple examples. First, this is an integer expression:
53 / 5 * 4 + 12 % 10 + abs(-7)
49
A float expression:
53. / 5. * 4. + 2.12 + sin(1.34)
45.49348454
A string expression:
strUpperCase( "Hello " + 'world!' )
HELLO WORLD!
A boolean expression:
false or true
true
Wafl supports many of the usual operators and functions for primitive types (integer, float, string and bool). There are also more complex types (list, array, map, tuple, record), which we will discuss in more detail later.
Simple expressions are often not enough. In the following sections, we will see how you can define and use functions and named expressions.
There are two types of comments in Wafl. Block comments start with
/*
and end with */
. Line comments start with
//
and end with the end of the line.
/* This is a block comment,
in two lines */
1 + 2 // This is a line comment after an expression
3
A tuple is a structured data type. It consists of a series of unnamed elements that are referenced by a position. The element of a tuple can have different types.
We will discuss tuples in detail in the following chapters. Now we just need to introduce tuples to make some of the following examples understandable. It is sometimes easier to represent many simple expressions in one example than to use many examples, and tuples are a good tool for this.
Tuples are written with curly brackets with hashes:
{# ... #}
, and tuple elements are separated by commas:
1, 2, 3.14, "Hello world!" #} {#
{# 1, 2, 3.14, 'Hello world!' #}
In the following example we use a tuple to evaluate 6 integer expressions:
{#7 - 6,
1 + 1,
6 / 2,
2 * 2,
12 % 7, // Remainder of integer division
-8 %% 7 // Similar to `%`, but always positive
#}
{# 1, 2, 3, 4, 5, 6 #}
A Wafl expression can define and use some local definitions. Local definitions are specified and used in where expressions. Local definitions include expression definitions and function definitions. Expression definitions are also referred to as named expressions.
In a nutshell:
Named expressions are used as values and functions are applied to some specific arguments.
The last sentence is a nice intuitive description, but do not take it too strictly. As we will see later, a named expression can have a functional type and be applied to arguments, and a function can be used as a value.
In the following example we define and use both a function and a named expression:
100, 0 ) + nam
fun( where {
fun( a, b ) = a * 10 + b;
nam = fun( 4, 2 );
}
1042
The function definition has name and parenthesis on the left side of the equality symbol, and named expression has only the name on the left side.
Local definitions are specified in where expressions. A where expression consists of a main part and a block of local definitions (where-block for short):
<main-expression-body>
where {
<definition>;
<definition>;
...
}
where each <definition>
represents an
expression definition or a function definition.
A function definition binds a function body to a function name and to its arguments. A function definition has the form:
<name>(<arguments>) = <expression>;
where <arguments>
is a list of the function
argument names. The arguments are separated by commas. The argument list
can be empty.
A definition of a named expression binds an expression to a name and has the following form:
<name> = <expression>;
where <name>
is any valid name and
<expression>
is (almost) any valid Wafl expression.
The most important restriction is that a name definition expression must
not contain a where
block.
The functions and the named expressions are by no means the same thing. Their semantics are very different. A named expression is something like a tool to make the expressions shorter and more readable, and it is of course strongly bound to the context. A function, on the other hand, is always free from the context in which it is defined. In the following section, we will discuss some specific details about local definitions and functions.
A function definition binds a function body to a function name and to its arguments:
<name>(<arguments>) = <expression>;
A function definition expression (<expression>
)
can use the following names:
In the next example, we can use the following in the expression body
of the function f
:
name_main
, defined in the main
where-block;name_f
, defined in its own
where-block;x
;f1
, defined in its own where-block;g
, defined in a parent where-block.For example, we could not use:
g1
in f
, because it is not
defined in a f
s parent where-block;name_f
in f1
, because it is not
defined in f1
s own where-block nor in main
where-block.'*')
f(where {
f(x) = x + name_main + name_f + f1(x) + g(x)
where {
name_f = x + x;
f1(x) = x + x + g(x);
};
name_main = '---';
g(x) = name_g + g1(x)
where {
name_g = x + x;
g1(x) = x + x;
};
}
*---************
A named expression definition binds an expression to a name and has the following form:
<name> = <expression>;
A named expression definition (<expression>
) can
use the following names:
In the following example, we can use the following in the definition
of the name name_f
:
name_main
, defined in the main
where-block;name_fa
, defined in its direct parent
where-block;x
, of the function f
in whose
where-block it is defined;f1
, defined in a parent where-block;g
, defined in a higher level parent
where-block.It is important to note that we cannot use recursive references to name definitions (either directly or indirectly) within a name definition expression, but we can use recursive references to a function in whose where-block the named expression is defined.
'*')
f(where {
f(x) = x + name_main + name_f + f1(x) + g(x)
where {
name_fa = name_main + x + x;
name_f = name_main + name_fa + g(x) + f1(x) + x + x;
f1(x) = x + x + g(x);
};
name_main = '---';
g(x) = name_g + g1(x)
where {
name_g = x + x;
g1(x) = x + x;
};
}
*---------************************
It is important to emphasize that the named expressions are not variables. Wafl has no variables. If a name is defined to represent an expression, then it represents the same expression throughout the evaluation of the scope in which the name is available. It is not possible to change the definition.
In the following example, x
is defined twice, so an
error is reported:
xwhere {
x = 1;
x = 2;
}
--- Loading: C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_867226_0.tmp
Parser error [C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_867226_0.tmp]
Definition name repeated! [err 1211:3]
Line 5: x = 2;
___/
--- End loading: C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_867226_0.tmp
A name can have different definitions in different scopes, even if one of the scopes is contained within another. If more than one definition of the same name is visible, the one with a narrower scope (i.e. closer to the place of use) is relevant.
In the next example, the name x
is defined twice:
f
);f
and in all its subdefinitions;f
;+ f( "10" )
x where {
x = "ABC"; // the first `x`
f( s ) = x + s + y
where {
x = "abc"; // the second `x`
y = "xyz";
};
}
ABCabc10xyz
If a named expression is defined in the where-block of a function and uses function arguments (either directly or indirectly), it can have different values in different function evaluations. However, this is not a problem, as it can only be used in the context of this function evaluation.
In the following example, the function f
is evaluated
twice, first for the argument 'a'
, and then for the
argument 'b'
. When evaluating f('a')
, the
name
is evaluated to '*a*'
and when evaluating
f('b')
it is evaluated to '*b*'
. As expected,
the values of the named expressions are not shared between the different
evaluations:
'a'), f('b') #}
{# f(where {
f(s) = s + name + s
where {
name = '*' + s + '*';
};
}
{# 'a*a*a', 'b*b*b' #}
An important aspect of the implementation of named expressions is efficiency. Each named expression is evaluated at most once per context. If it is never used, it is never evaluated, but if it is used several times, it is only evaluated once.
In the following example, the value of the named expression is never requested, so it is never evaluated. We know this for sure, because its evaluation would generate an error:
if 2 < 5
then 5
else error
where {
error = 1/0; // Zero division!
}
5
In the following example, we use the same name expression many times. However, we always get the same value, as it is evaluated at most once:
{# x, x, x, x, x, x, x, x, x, x #}where {
x = random(1000);
};
{# 780, 780, 780, 780, 780, 780, 780, 780, 780, 780 #}
Future Wafl versions might analyze whether the definition body is
deterministic or non-deterministic. If the definition
is non-deterministic, then its semantics can be defined differently, so
that it is re-evaluated each time. In that case, the previous example
might give different results. Non-deterministic behavior would include
the use of the random
function, but also the use of files,
networks and so on.
The order of the definitions in the same scope (i.e. in the same where-block) is not important. However, it is good to use either a top-down or a bottom-up strategy to make the program easier to read. A top-down strategy suggests defining the most abstract name first, even if its definition uses some other names that will be defined later. In contrast, a bottom-up strategy suggests introducing the simplest definitions first and using them later in more abstract definitions.
In the following example, we define the same simple function three times (with different names), to demonstrate different styles:
'a'), bottomUp('a'), noOrder('a') #}
{# topDown(where {
topDown(x) = a
where {
a = b + b + x + c;
b = c + x + c;
c = "#" + x + "#";
};
bottomUp(x) = a
where {
c = "#" + x + "#";
b = c + x + c;
a = b + b + x + c;
};
noOrder(x) = a
where {
c = "#" + x + "#";
a = b + b + x + c;
b = c + x + c;
};
}
{# '#a#a#a##a#a#a#a#a#', '#a#a#a##a#a#a#a#a#', '#a#a#a##a#a#a#a#a#' #}
The three definitions are equivalent to each other, but the last one is more difficult to read.
if
In imperative programming languages, it is common to use conditional statements to control the flow of program execution. Wafl is a functional language and does not contain statements, so in Wafl we use conditional expressions instead.
The Wafl programming language has two types of conditional
expressions: if
and switch
. The expression
if
contains a condition and two optionally evaluated
expressions:
if <condition> then <then-exp> else <else-exp>
where <condition>
must be an expression of the
logical type Bool
, while the expressions
<then-exp>
and <else-exp>
can be
of any type, but must have the same type.
The conditional expression is evaluated first. If its value is
true
, then the expression <then-exp>
in
the then
branch is evaluated and the resulting value is the
result of the full if
expression. In the other case, if
<condition>
has the value false
, then
the expression <else-exp>
in the else
branch is evaluated and the resulting value is the result of the
complete if
expression.
{#if 5 > 2 then 'five' else 'two',
if 5 < 2 then 'five' else 'two'
#}
{# 'five', 'two' #}
switch
The conditional expression if
evaluates one of the two
alternative expressions. If more alternatives are required, we can use
multiple if
expressions or a
switch
-expression.
The switch
expression consists of a conditional
expression, at least one case clause and a mandatory
default clause:
switch <cond-exp> {
<case-clause>;
<case-clause>;
...
<default-clause>
}
The conditional expression is evaluated first. Then, based on the resulting value, one of the case clauses is selected and evaluated, or, if none is selected, the default clause is evaluated. The result of the selected and evaluated clause is the result of the switch expression.
Each case clause begins with the keyword case
, followed
by a comma-separated list of values. The list of values is followed by
an expression that ends with a semicolon:
case <literal>, ... <literal> [:] <exp>;
An optional colon symbol can separate the value list from the clause
expression. In addition, the keyword case
can be used as a
separator instead of a comma and an optional colon symbol can be used in
front of it. So, case 1,2,3
is the same as
case 1: case 2: case 3
.
The default clause is mandatory. It is defined as:
default [:] <exp> [;]
The following example uses both types of conditional expressions to calculate how many days a given month has:
{#1, 2000 ),
daysInMonth( 2, 2000 ),
daysInMonth( 2, 2001 ),
daysInMonth( 2, 2004 ),
daysInMonth( 2, 2100 )
daysInMonth(
#}where{
daysInMonth( month, year ) =
switch month {
case 2:
if isLeapYear(year)
then 29
else 28;
case 4, 6, 9, 11:
30;
default:
31
;
}
isLeapYear(year) =
% 4 = 0
year % 100 != 0
and ( year % 400 = 0 );
or year }
{# 31, 29, 28, 29, 28 #}
The conditional expression <cond-exp>
is evaluated
first. All case clauses are checked in the specified order to see if
there is a literal that matches the conditional value. If such a case
clause is found, the corresponding expression is evaluated and its value
is the result of the complete switch
expression. If no
specified literal matches the value of the
<cond-exp>
, the expression of the default clause is
evaluated and its value is the result of the complete
switch
expression.
There are three simple rules:
One could say that it is unusual that the syntax here is so flexible, but it was introduced to create a syntactic similarity to C-like languages.
Since Wafl is a functional programming language, it has no variables, so iterative processing is (almost) impossible. The main method to express “repetitive” behavior of functions is the use of recursion.
Recursion is a very important technique that allows a function definition to use the function itself. It is often used in mathematics as a tool for defining infinite (but still enumerable) structures.
The usual example of a recursive definition in mathematics is the
definition of factorial function. The factorial, which is
usually denoted by the postfix operator !
, is defined as
follows:
0! = 1
(n+1)! = (n+1) *
n!
The definition of every recursive function is based on two main elements:
the recognition of the terminal condition, i.e. the case where the arguments allow a non-recursive, direct evaluation of the result, and
the definition of the recursive evaluation in such a way that the application of each step brings the evaluation closer to the terminal condition.
Both recursion elements are of great importance. We need to consider them carefully when using recursion. If the terminal condition is not well defined, the evaluation of a recursive function may never finish for some arguments (or at least not in an acceptable time frame). On the other hand, if a recursive step is not well defined, the efficiency of the function may suffer for some arguments.
In the case of the factorial function, the terminal condition is met
if the argument is zero. In this case, the result can be evaluated
directly. In other cases, the result is defined based on the application
of the same principles for smaller arguments. Following these rules, we
can evaluate n!
in n
+1 steps.
In the following example, we define and use the function
factorial
:
{#0),
factorial(1),
factorial(2),
factorial(3),
factorial(4),
factorial(5),
factorial(16)
factorial(
#}where{
factorial( n ) =
if n<=1 then 1
else n * factorial(n-1);
}
{# 1, 1, 2, 6, 24, 120, 20922789888000 #}
Another example of recursive definitions are the Fibonacci numbers. The first and second elements of the sequence are defined as 1, while every other element is defined as the sum of the two preceding elements:
Fib0 = 1
Fib1 = 1
Fibn+2 =
Fibn+1 +
Fibn
And here is the function fib
, which evaluates a given
element of the Fibonacci number sequence:
{#1),
fib(2),
fib(3),
fib(4),
fib(5),
fib(6)
fib(
#}where{
fib( n ) =
if n<=2 then 1
else fib(n-1) + fib(n-2);
}
{# 1, 1, 2, 3, 5, 8 #}
In imperative programming languages, recursion is usually taught as an exotic technique. It is usually noted that while it looks nice, it is not efficient and can cause other problems. For example, deep recursion can destroy the organization of the internal memory of imperative programming languages (where “deep” usually means close to a thousand or several thousand steps).
Do not worry about this in Wafl. As a functional programming language, Wafl relies on recursion as one of its elementary techniques. You are free to use deep recursion, but be careful - there will be no memory corruption, but very deep recursion can still cause problems due to a high memory usage. But in Wafl “deep” is not measured in hundreds and thousands, here we are talking about hundreds of millions.
Knowing this, it may seem strange now, but there are enough advanced constructs in the programming language that programmers do not need to explicitly iterate in most cases, and deep recursions are not often used in reality. We will look at lists and higher order functions in the next chapter and learn how to program efficiently.
Wafl libraries are used by declaring a local library name and binding it to an externally defined library.
Regular Wafl libraries are files with the following syntax:
<LIBRARY_FILE> ::=
library [<name>] { <definition>* }
[<lib_where_subdefinitions>]
<lib_where_subdefinitions> ::=
where { <definition>* }
Each library begins with a declaration
library [<name>]
. The name specified in the library
is used exclusively for the documentation. It can contain a version
number, as long as it follows the Wafl syntax for names.
The declaration is followed by a block of public definitions. The
public definitions can be followed by an optional where
block with private definitions.
The following example shows a simple library with a public function
f
and a private function g
:
library Example {
= g( x ) + g( x );
f( x )
}where {
g( x ) = x + x ;
}
To use a library in Wafl programs (and in other libraries), the
library must be declared. Each library declaration must be specified in
the where
block. It represents the definition of a name as
a library reference. Library references are normally placed at the end
of the outermost where
blocks of programs and
libraries.
A library reference has the following syntax:
<name> = library [file] <filename>
where <filename>
must be specified as a string
literal.
For example, if the previous library example is available as the file
example.wlib
, then a corresponding library reference can be
declared as follows:
= library file 'example.wlib'; elib
The specified name (elib
in the example) is used in the
program to refer to the library. The name of the library, which is
defined in the library, is not used for referencing. This approach
allows us to use different versions of the same library at the same time
by simply using different reference names:
= library file 'v1/example.wlib';
elib1 = library file 'v2/example.wlib'; elib2
However, we strongly recommend using the same reference names for all libraries in a project.
A library definition is comparable to namespaces in C++ or package
names in some other languages. To use a name defined in a library, it
must be preceded by the library name and the scope operator
::
.
For example, to use the function f
, which is defined in
a library declared as elib
, we must write
elib::f
.
elib::f( "A" )
where {
elib = library file 'example.wlib';
}
"AAAA"
Library documentation can be created automatically in Markdown format using a command:
clwafl -doc <lib_file_name>
For example, we can use the following command to create a
documentation for the example.wlib
library:
clwafl -doc example.wlib > example_wlib_doc.md
A library documentation contains:
Library names and definition names and types are automatically extracted from the code. Definition descriptions are extracted from the comments in a Doxygen-like manner:
The supported formats for documenting comments are:
///
for single-line comments and
/** ... */
for multi-line comments.
All documenting comments that precede a library declaration or a function definition are extracted and used as the corresponding descriptions.
Markdown elements can be used in the documenting comments.
In some cases, we do not need to select a specific library during development, but only when the program is used. If we provide several different libraries with the same interface (public names), we can parametrize the program evaluation by specifying a selected library. Different libraries can, for example, use different databases or different file formats.
To specify a library when using a program, we use parametrized libraries. A parametrized library declaration consists of two parts. First, the parametrized library is declared in the program using the following syntax:
<name> = library param [<libname>]
where <libname>
is a parametrized library name. If
no <libname>
is specified, the name
<name>
is used as the default value instead.
When executing the program, the parametrized library name must then be bound to a library file name using the command line parameter:
-lib:<name>:<filename>
where <name>
is a parametrized library name and
must correspond to the name used in the program, and
<filename>
is a library file name, to be used as a
parametrized library instance.
For example, we can write a program as:
elib::f( "A" )
where {
elib = library param;
}
and execute it with:
clwafl -lib:elib:example.wlib program.wafl
to get the same output as before:
"AAAA"
The Wafl loader first tries to load the parametrized library as a regular Wafl library. If the file is not found or is not a regular Wafl library, the loader attempts to load a dynamic library.
Dynamic Wafl Libraries are libraries written in C and C++.
The dynamic libraries are distributed as .so
or
.dll
files. They are used in a similar way to regular Wafl
libraries, but with a specific declaration syntax:
<name> = library extern [cpp] <libname>
where <libname>
can be an exact library file name
or a generic library name. The Wafl loader attempts to load one of the
following files, depending on the platform:
<libname>
<libname>.so
libw<libname>.so
<libname>.dll
libw<libname>.dll
To list the contents of a dynamic library, use clwafl
with the command line option -listlib:<libname>
.
The development of dynamic libraries is outside the scope of this tutorial. Please contact the developers for more information.
Universal library declarations assume the implicit recognition of library types instead of using explicit library type declarations. The syntax of the simple library declaration is:
<libname> = library [<libfilename>];
The <libfilename>
is optionally specified as a
literal string. The following declaration specifies, for example, that
the library MyLib
is first searched for a binary dynamic
library libwAlib.dll
(or libwAlib.so
); if no
binary library is found, a regular Wafl library Alib.wlib
is used:
MyLib = library 'Alib';
If <libfilename>
is not specified, the library
name <libname>
is used by default. In the following
example, it is the same as if the library file name is specified as
'MyLib'
, so that a binary dynamic library
libwMyLib.dll
(or libwMyLib.so
) is searched
for first; if no binary library is found, then a regular Wafl library
MyLib.wlib
is used.
MyLib = library;
The libraries are searched for in the following directories:
<ref>
<ref>/lib
<libdir>
${WAFL_PATH}/bin
(only for dynamic libraries)${WAFL_PATH}/bin/lib
(only for dynamic libraries)${WAFL_PATH}/lib
<prg>
<prg>/lib
<prg>/bin
(only for dynamic libraries)where:
<ref>
is the location of the file from which the
library is referenced;<libdir>
is the location specified as the
-libdir
interpreter option;WAFL_PATH
is an environment variable pointing to the
root directory of the Wafl package installation;<prg>
is the location where the currently used
Wafl program module (interpreter) is located.