Wafl is a strongly typed functional programming language with implicit type inference. It was originally designed as an experimental application of FP languages in web development. A few years later, however, Wafl became a simple and very fast scripting language, with a simple and efficient interface to databases.
The Wafl Tutorial introduces the language constructions, the use of the command line interpreter, elements of the core library and many examples.
Welcome to Wafl!
Saša Malkov
This introductory chapter covers the basic elements of Wafl syntax and the use of the command line interpreter. The aim is to provide some basic information that will enable us to progress further in the following chapters.
For most of this tutorial, we assume that you are using the Wafl
command line interpreter. The name of the command line interpreter may
vary depending on the installation package version. It can be
clWafl, clwafl, wafl, or simply
wl. In this tutorial we assume that it is called
clwafl.
To check if the interpreter is ready, open the OS command shell and type:
clwafl
If everything is set up correctly, the output should look like this:
*** Program filename missing!
*** Wafl Command Line Interpreter usage syntax:
clwafl [<options>] <program file name> [<arguments>]
clwafl [<options>] -code <source code> [-args <arguments>]
clwafl -help
We assume that each program is written and saved in a file and then
evaluated with the command line interpreter. To evaluate a program
stored in example.wafl, enter and execute:
clwafl example.wafl
Command line interpreter options are discussed in details in Command Line Reference.
To download Wafl, visit Wafl Downloads. The current version is available as a Linux deb package and as a Windows installation program. Each installation package contains an offline version of this tutorial.
The easiest way to display the string "Hello World!" is
to write a program that consists only of this string:
"Hello World!"Hello World!
If you write this program and save it as a new file with the name
“hello.wafl”, you can evaluate it with the Wafl command
line interpreter:
clwafl hello.wafl
If everything is set up correctly, the output should look like this:
Hello World!
Wafl is a strongly typed and eager functional programming language with implicit type inference.
Let us discuss the first sentence in detail.
Wafl is a functional programming language. A Wafl program has the syntactic form of an expression. In imperative programming we speak of the execution of a program, but here we speak instead of the calculation or evaluation of a program. The order of program execution is not explicitly determined by the program source file, but by implicitly defined rules for the evaluation of expressions.
Wafl is not a pure functional language. There are some language features that violate the strict rules of the functional paradigm, including the command line interaction subsystem, the use of files and databases, and so on. However, the core of the language is defined in such a way that we can speak of a large pure subset. Most of the examples in this tutorial use only the pure subset of the language. Impure elements are generally limited to some elements of the core library.
There are almost no variables, assignments, iterations, procedures, or other elements common in imperative programming languages. Even if there are some types of these concepts, they are not present in the usual imperative form.
The order of evaluation is not important in most cases, but where it is important, it can be assumed that all arguments of functions and operators are evaluated from left to right.
Wafl is an eager language. This means, in short, that for most functions (and operators) all arguments are fully evaluated before the function (or operator) is evaluated. There are some common exceptions, such as logical operators and conditional expressions.
However, there are also some lazy elements. The most important lazy subsystem is the processing of database queries. The query results are not retrieved completely before the query results are processed. Instead, the query result is treated as a lazy list of rows. Individual rows of the query result are retrieved as needed.
Wafl is a strongly typed programming language. Each program is completely analyzed and type-checked before evaluation. No type-related errors can occur during program evaluation. However, the Wafl syntax does not contain an explicit type specification. All types are implicitly inferred. Normally, the derived types are not displayed during the evaluation, but if a type error is detected during type checking, the corresponding derived types are reported to support problem localization.
To understand function type notation, we first need to understand type notation in general. This requires at least a small introduction to the Wafl type system. For exact types the notation is relatively simple, but for polymorphic types it can be more complex.
The types are never written in Wafl programs. The notation of types is only important because it is used in the interpreter’s type-checking error messages and in the Wafl documentation (including the library reference and this tutorial).
Wafl supports implicit polymorphism. This means that each function
has a strictly defined type, but is as general as possible so that it
works for all types to which its definition can apply. For example, we
can define a function add3:
add3(x,y) = x + y + 3;This is a definition of the function add3. It is easy to
verify that add3 can only work for integer arguments
(because no implicit conversions are allowed and we have an explicitly
specified integer argument: 3). We can write the type of
this function as follows:
(Int * Int -> Int)
Function types are always enclosed in parenthesis. This type
indicates that the function has two arguments of type Int
and the result of the same type Int.
Let us now define a function add:
add(x,y) = x + y;The function add can work for all types for which its
definition expression can be evaluated correctly. This includes all
types for which the binary operator ‘+’ is defined. In
Wafl, these are the primitive types: Integer,
Bool and String. The function add
therefore has a polymorphic type that includes the exact types
(Int * Int -> Int),
(Float * Float -> Float) and
(String * String -> String). To simplify the notation,
we use the parameterized notation:
(Value['1] * Value['1] -> Value['1])
The Value represents a particular set of types
(Int, Float, String) and
Value['1] represents an instance of the set, where
'1 is a type parameter (or type
variable). Type parameters take the form of a quotation mark
followed by an integer. A type parameter can represent any type that
fits the context (i.e. any type from the given set), but the same type
in the entire type notation. In this example, '1 is one of
the types in the set Value - Int,
Float or String, but the same in all three
occurrences.
So, if we replace '1 with specific types, one by one,
then our type (Value['1] * Value['1] -> Value['1])
virtually becomes a set of types that includes:
(Int * Int -> Int),
(Float * Float -> Float) and
(String * String -> String).
Now, we agree that it was almost more complicated to explain this
than to use the set of types. But there are sets that are too large to
be written without a special polymorphic notation. For example, if we
have a polymorphic list of elements, then its type is denoted as
List['1], where '1 denotes the element type
and can be any type that Wafl supports. Thus,
List['1] represents an infinite set of types.
List['1] is an infinite set, at least theoretically. If
there is a List[A] for a type A in this set,
then there is also a type List[List[A]] in this set, and so
on… For every natural number N there is at least one
N-deep list, so our set of list types is at least as large
as the set of all natural numbers, which is infinite.
Let us write a function that checks if a list is longer than a given number:
isLongerThan( list, n ) = size( list ) > n;First, we know that size is a core library function that
returns Int, so n must also be of type
Int. The expression of the function definition evaluates to
a Bool value, so we conclude that our function will have a
type like (... * Int -> Bool). And what is the type of
the first argument?
In our example, we expect a list as the first argument, but there are
no any implications for its element type. Then it will be a polymorphic
list List['1], and the function type will be:
(List['1] * Int -> Bool).
But size works not only for lists, but also for arrays.
To consider all sequence types (lists and arrays), the function type
must be more general:
(Sequence['2]['1] * Int -> Bool
where '2 stands for a sequence type (array or list) and
'1 for a sequence elements type.
In addition, size also works for strings. Strings often
behave like sequences of characters (even though there is no new type
for characters - String is used), so we have a more general
type class SequenceStr that includes arrays, lists and
strings:
(SequenceStr['2]['1] * Int -> Bool
If '2 is String, then '1 must
of course also be String.
But that’s not all. There is a map type in Wafl, that is a
collection of elements accessed via keys. Its type is
Map['2]['1], where '1 is the element type and
'2 is the key type. And of course the function
size also works for maps. So here is the final type of our
function isLongerThan:
(Indexable['1]['2]['3] * Int -> Bool)
where '1 is the type of the indexable collection (list,
array, map or string), '2 is the type of the keys (for all
collections except maps must be Int) and '3 is
the type of the elements.
Here is a list of some of the type classes we often use in Wafl:
Type Class / Description
Prime['1]
One of the primitive types: Int, Float,
Bool and String
Value['1]
One of the types: Int, Float and
String.
Numeric
One of the types Int and Float.
PrimeNotInt['1]
One of the types: Float, Bool and
String.
PrimeNotFloat['1]
One of the types: Int, Bool and
String.
PrimeNotString['1]
One of the types: Int, Float and
Bool.
Sequence['2]['1]
A sequence, where '2 specifies the sequence class
(array, list) and '1 the element type.
SequenceStr['2]['1]
A sequence, where '2 specifies the sequence class
(array, list or string) and '1 the element type. If
'2 is String, then '1 must also
be String.
Indexable['3]['2]['1]
An indexable collection, where '3 specifies the
collection class (array, list, map or string), '2 the index
type, and '1 the element type.
There are a few other types and type classes, but this should be enough to get you started.
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+36
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.49348454169532
A string expression:
strUpperCase( "Hello " + 'world!' )HELLO WORLD!
A Boolean expression:
false or truetrue
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 expression3
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:
fun( 100, 0 ) + nam
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 fs parent where-block;name_f in f1, because it is not
defined in f1s 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:
x
where {
x = 1;
x = 2;
}--- Loading: C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_989978_0.tmp
Parser error [C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_989978_0.tmp]
Definition name repeated! [err 1211:3]
Line 5: x = 2;
___/
--- End loading: C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_989978_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;x + f( "10" )
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:
{# f('a'), f('b') #}
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);
};{# 600, 600, 600, 600, 600, 600, 600, 600, 600, 600 #}
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:
{# topDown('a'), bottomUp('a'), noOrder('a') #}
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.
ifIn 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' #}
switchThe 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:
{#
daysInMonth( 1, 2000 ),
daysInMonth( 2, 2000 ),
daysInMonth( 2, 2001 ),
daysInMonth( 2, 2004 ),
daysInMonth( 2, 2100 )
#}
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) =
year % 4 = 0
and ( year % 100 != 0
or year % 400 = 0 );
}{# 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:
{#
factorial(0),
factorial(1),
factorial(2),
factorial(3),
factorial(4),
factorial(5),
factorial(16)
#}
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:
{#
fib(1),
fib(2),
fib(3),
fib(4),
fib(5),
fib(6)
#}
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 {
f( x ) = g( x ) + g( 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:
elib = library 'example.wlib';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:
elib1 = library 'v1/example.wlib';
elib2 = library 'v2/example.wlib';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"
From version 0.6.9, the importing syntax is supported. It has a form of a declarative statement, which must stand at the very beginning of a program. Import declaration can have different forms, and each of them is equivalent to a regular library declaration form:
import libname; - The basic form is equivalent to a
declaration libname = library;, or
libname = library 'libname'; The library manager will first
try to load a binary library libname with an appropriate
extension (.dll or .so), and with added prefix
libw if necessary. If no binary library is found, then it
will try to load a regular Wafl library libname.wlib.import 'libname.ext'; - This form is similar to the
basic one, but no extension will be added implicitly. It is the same as
libname = library 'libname.ext';.import libname = libfilename; - This form explicitly
defines a library name which is not the same as the file neme. It is the
same as libname = library 'libfilename';. The extension may
be specified, also.import libname = param pname; - This form corresponds
to a parametrized libraries declaration
libname = library param 'pname';. See Parametrized Libraries for
more detail.From version 0.6.10, the using syntax is supported. It is similar to the importing syntax with two significant differences:
using keyword is used instead of
import;It is still possible to use the full library name referencing as with
import and the library declaration.
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 binary library.
Binary Wafl Libraries are libraries written in C and C++.
The binary 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>.solibw<libname>.so<libname>.dlllibw<libname>.dllTo list the contents of a binary library, use clwafl
with the command line option -listlib:<libname>, or
the equivalent shorthand ? <libname>. For more
details add the option -verbose or use
?? <libname>.
Each binary library includes the function _libData_(),
that returns some general information on the library:
Timer::_libData_()
where { Timer = library; }{ build: 'RELEASE 64b', name: 'Timer', verMajor: 0, verMinor: 6, verPatch: 10, verStrFull: '0.6.10 (25091)', verStrPublic: '0.6.10', verTweak: '25091' }
The development of binary 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 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 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<app-libdir><app-libdir>/lib<app-cwd><app-cwd>/lib<sys-libdir><prg><prg>/lib<prg>/../libwhere:
<ref> is the location of the file from which the
library is referenced;<app-libdir> is the application library
directory, usually specified as the -libdir interpreter
option or libdir ini-file option; defaults to current
working directory;<app-cwd> is the root directory of a Wafl
application (service); it is specified when application/service is
started, in a way that depends on the service type;<sys-libdir> is the system defined library
directory, usually specified as libdir-system ini-file
option; default value is ${WAFL_PATH}/lib;<prg> is the location where the currently used
Wafl program module (interpreter) is located.Wafl is a strongly typed programming language. This means that Wafl requires a detailed and very strict type checking before program evaluation. Each function application must fully comply with the function type.
Strong type checking is usually paired with explicit declarations of name types. In Wafl, however, no explicit type declarations are required as the types are automatically inferred during the code analysis.
Due to the strict type checking, no implicit type conversions are allowed.
In the following example, we have a program with incorrect type usage. The addition operator is defined for various types, but it is not defined for operands of different types. An error is therefore reported:
1 + '2.0'--- Loading: C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_878529_0.tmp
Typechecking failed!
- Program [Line: 2]
The argument (2) type is not suitable for the context:
1 + '2.0' [Line: 2]
The expression type is not suitable for the context:
'2.0'
Exp.type is: String
Not compatible with: Int
Before simplification: Value['2]
--- End loading: C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_878529_0.tmp
The correct way to deal with such problems is to use explicit type conversions. This will be covered later in this tutorial.
Wafl is a strongly typed programming language. However, the Wafl syntax does not contain explicit type specification. All types are inferred implicitly. Normally, the inferred types are not displayed to user during program evaluation, but if a type error is detected during type checking, the related inferred types are reported.
In the following example, three local functions are defined:
g is recognized to be of type
(Float -> Float) (maps a single float argument to a
float result);h is recognized to be of type
(Int -> Int)(maps an integer argument to an integer
result).f is not well defined because there is an
addition operator a first operand of type Float and a
second operand of type Int, but the implemented addition
operator does not allow the different operand types.f(7.0)
where{
f(x) = g(x) + h(x); // Type checking error!
g(x) = sin(x);
h(x) = x + 5;
}--- Loading: C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_827585_0.tmp
Typechecking failed!
- f ( x ) : '2 [Line: 4]
The argument (2) type is not suitable for the context:
g(x) + h(x) [Line: 4]
The result type is not suitable for the context:
h(x) [Line: 4]
Exp.type is: (Int -> Int)
Not compatible with: Float
Before simplification: Value['5]
--- End loading: C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_827585_0.tmp
The error report contains some redundant data, as all branches of the type checking process are reported to allow a better understanding of the reported problems. If we read the report from the beginning, we can see all the actions that the type checker has performed:
f failed
because f uses the names g and h
whose types have not yet been inferred;g
inferred its type : (Float -> Float);h is inferred also:
(Int -> Int);f and
failed again because h(x) is not a valid usage in the
context, which means that x is already assumed to have a
non-float type;f, but
now in a wider context and failed for the same reason.Sometimes, functions use mutual recursion, i.e. they use each other and form a recursive circle. In this case, all these functions must be checked together and simultaneously. Therefore, the type checking is performed in a broader context if a local type checking has failed.
In most cases, it is better to read the report from the bottom. The
last block of reported unrecognized types is the most important. Any
line in the report beginning with OK: indicates that the
given definition type is well inferred.
Reading from the bottom up, the error report shows that the type
checking for Program and f fails and the
problem is in the subexpression h(x). Further analysis
shows that this is h(x) from the definition body of
f.
Polymorphism is the property of a function to be well defined for arguments of different data types. Implicit polymorphism is a very important feature of the Wafl programming language.
Type inference starts with the most general assumption that each of the local definitions is completely polymorphic, i.e. that any argument of any function can be of any data type. Step by step, the assumed polymorphic types are reduced to types for which expressions and definitions are applicable.
In the following example, two polymorphic functions add
and sub are defined and used with arguments of different
types:
{#
add(1,2),
add(1.3,2.3),
add('a','b'),
sub(5,1),
sub(5.4,1.2),
f(5,4,3),
f(1.2, 3.4, 1.3),
add( asFloat(add(1,2)), 3.1 )
#}
where{
add(x,y) = x + y;
sub(x,y) = x - y;
f(x,y,z) = sub( add(x,y), z);
}{# 3, 3.5999999999999996, 'ab', 4, 4.2, 6, 3.3, 6.1 #}
The function add has the type
(Value['1] * Value['1] -> Value['1]). This means:
Integer, Float or String);Similarly, the type of the function sub is inferred to
be (Numeric['1] * Numeric['1] -> Numeric['1]). Thus:
Integer or Float);Both Numeric['1] and Value['1] are
so-called combined data types. They can be further reduced to
different primitive data types. The type '1 stands for a
polymorphic type name, like a type variable. Identical
polymorphic type names in a type must be reduced to identical specific
types in each specific function application.
In each particular function application, the function type must be reduced to a non-polymorphic data type. However, different applications of the same polymorphic function can be reduced to different types in a program. When used in function definitions, polymorphic functions can be used in a polymorphic way, without further reduction of the data types, or with reduction to a more specific but still different polymorphic type. Their types are further reduced if the application of the defining function makes this necessary.
The function f in our example is defined as:
f(x,y,z) = sub( add(x,y), z);and its type is inferred as
(Numeric['1] * Numeric['1] * Numeric['1] -> Numeric['1]).
Thus:
Integer or Float);The function f uses both add and
sub. In the specific application, the type of the function
add is reduced from Value['1] to
Numeric['1], while the type of the function
sub is not reduced any further.
It is important to note that each specific application is
reduced independently of the other applications. Therefore, in the same
example, our function add is successfully applied to a pair
of integers, a pair of floats and a pair of strings, and the functions
sub and f are successfully applied to integers
and floats. Also, in the last expression, the function add
is applied to both integers and floats.
The function asFloat converts a value from integer to
float. The conversion functions are covered later in this tutorial.
One of the essential features of functional programming languages is that functions are treated as first-order citizens. Functions can be used as arguments and results of other functions, in a much simpler way than in the most imperative languages.
The functions that take functions as arguments or return a function as a result are called higher order functions. They are essential for functional programming.
In the following simple example, the function double is
used as an argument to the function apply:
apply(double,2)
where{
apply(f,x) = f(x);
double(x) = x + x;
}4
The function double has the type
(Value['1] -> Value['1]). It is similar to the
add function from one of the previous examples.
The function apply has the type
(('1 -> '1) * '1 -> '1). This means:
('1 -> '1);'1,'1 and'1 can be replaced by
any type, but in the same time in all previous
assertions.In the given example, the function apply applies the
function double to 2. In this particular
application, the type variable '1 is replaced by
Int.
More complex examples follow later.
A partial application is any function application where not all expected arguments are specified.
For example, if we were to specify only the first argument of the
add function (from the previous examples), the result would
be a unary function that returns its argument incremented by one.
One way to do this is to define a new function:
f(x) = add( 1, x );Another possibility is to use a partial application:
f = add(1,_);In Wafl, the syntax of the partial application is similar to a
regular function call, but the underscore ‘_’ is used
instead of unspecified arguments. You must always use as many
arguments as the function expects, but some of the arguments can be
specified and others can be left unspecified.
The result of a partial application is a function. Its arguments correspond to the unspecified arguments of the partial function application, in the preserved order.
It is possible to leave all arguments unspecified. However, this is not very useful as the result is the function itself.
In the following example we use the function
subStr(str,pos,len). It extracts a substring of
str that starts at position pos (zero based)
and is len characters long. The string functions are
discussed in detail later.
In the following example, several partial applications are used:
{#
strGetFirst5a( "1234567890" ),
strGetFirst5b( "1234567890" ),
charAt( "123456", 1 ),
charAt( "123456", 2 ),
add2( 5 ),
// The following expressions give the same result
subStr( "1234567890", 5, 3 ),
subStr( _, 5, _ )( _, 3 )( "1234567890" )
#}
where{
// The following definitions are identical.
// Both functions return
// the first 5 characters of a string.
strGetFirst5a( s ) = strLeft( s, 5 );
strGetFirst5b = strLeft( _, 5 );
// charAt(n) returns the nth character of a string.
charAt = subStr( _, _, 1 );
// twice(f,x) applies f twice to x
twice( f, x ) = f( f(x) );
// inc(x) returns x incremented by 1
inc(x) = x + 1;
// add2 returns its argument incremented by 2
add2 = twice( inc, _ );
}{# '12345', '12345', '2', '3', 7, '678', '678' #}
In many functional programming languages, a partial application is implemented by specifying only the first argument of a function (or, more generally, some of the arguments from the beginning of the argument list, in the appropriate order). This approach is called curried functions. The result of a curried partial application is a function that maps the remaining arguments to the result.
The partial application, as implemented in Wafl, is more general than curried functions.
A reader may wonder what a partial application is good for. As with several other topics in this part of the tutorial, its purpose will become clear as we introduce more advanced sequence processing techniques.
Higher order functions are so commonly used in functional programming that we need as many methods as possible to define or create new functions. Partial application is one such method. Lambda functions are another, as we will see in the next section.
In functional programming languages, the term lambda function refers to various concepts for defining unnamed functions at the place where they are used.
When we use higher-order functions, we often have to specify a function as an argument that is not needed elsewhere. If we use a function only once in a program, it is easier and often more readable to define it inline than to introduce a regular function definition.
In the Wafl programming languagewe define lambda functions in the following form:
\ <arg1>, ..., <argn>: <exp>
A lambda function can appear at any point where we can use a regular function name. Sometimes it is necessary to put the lambda function in parenthesis to clarify the syntax.
We will often use the simplified term lambda instead of lambda function or unnamed function.
In our first lambda example, we define and use an unnamed function that returns its argument incremented by 1:
(\x:x+1)(41)42
This function is the same as the inc function from the
previous examples, but now it is defined as a lambda and used in the
place of the definition. This is much shorter than using a full
definition syntax, as in the following example:
inc(41)
where {
inc(x) = x+1;
}42
The following examples are more complex than the previous examples. It may be more difficult for beginners in functional programming to understand them. But such examples are essential - please read carefully.
For the first time, let us define a function that takes only
functions as arguments and returns a new function as a result. The
function combine takes two functions as arguments. It
returns a unary function that applies the combined functions to its
argument. The following expression should therefore always evaluate to
true:
combine(f,g)(x) == f(g(x))One way to write combine is to first define an auxiliary
function c that takes three arguments f,
g and x and returns their partial
application:
combine = c(f,g,_)
where {
c(f,g,x) = f(g(x));
}A simpler way is to use lambda instead of the definition of the
function c, but the solution is equivalent:
{#
add2(1),
add3(1),
combine( inc, inc )(10),
combine(\x:x+1,\x:x+1)(10)
#}
where{
combine( f, g ) = ( \f,g,x : f(g(x)) )( f, g, _ );
add3 = combine( add2, inc );
add2 = combine( inc, inc );
inc(x) = x + 1;
}{# 3, 4, 12, 12 #}
In the next section, we will see that combine can be
implemented even more simply.
In the following example, we use combine to define the
function repeat that generalizes twice from
the previous examples. For a given function f and a given
integer n, repeat returns a function that
applies f to its argument n times. In
repeat we use the lambdas to write the identity function,
and in the main expression we use a lambda equivalent of the function
inc:
{#
repeat( \x:x+1, 0 )( 100 ),
repeat( \x:x+1, 1 )( 100 ),
repeat( \x:x+1, 10 )( 100 )
#}
where{
repeat( f, n ) =
if n=0
then \x:x
else combine( f, repeat(f,n-1) );
combine( f, g ) = ( \f,g,x : f(g(x)) )( f, g, _ );
}{# 100, 101, 110 #}
Note that this implementation of repeat is not very
efficient. However, it shows how higher-order functions can be combined
with lambdas to achieve very interesting goals.
In functional programming languages, a closure is a function that binds some elements of its outer scope. We can say that partial applications are a kind of closures.
Let us go back to the definition of the function
combine. We can say that combine binds the
given functions f and g in a closure:
combine( f, g ) = ( \f,g,x : f(g(x)) )( f, g, _ );Definitions like this are very common: first we need to define a function with multiple arguments and then create a closure by partially applying this function. It seems like we write the same things twice.
Fortunately, we can use lambda closures (or lambdas with bindings), to create closures more easily. To define a lambda closure, we need not only the lambda arguments, but also the list of names from the definition space that we need to bind. These names are listed in square brackets, after the lambda arguments:
\ <arg1>, ..., <argn> [ <name1>, ..., <namek> ] : <exp>
There is an alternative, older syntax for the same thing, where a
hash symbol ‘#’ is used to separate the list of lambda
arguments from the list of bound names:
\ <arg1>, ..., <argn> # <name1>, ..., <namek>: <exp>
Now we can define the function combine even more easily
than before with the help of a lambda closure:
{#
combine(\x:x+1,\x:x+1)(10),
combine(operator+(2,_), operator*(8,_))(5)
#}
where{
combine( f, g ) = \x [f,g]: f(g(x));
}{# 12, 42 #}
The syntax for partial application is not applicable to operators.
Furthermore, operators cannot be used as arguments for function
application. For this reason, Wafl introduces the functional syntax of
operators. Each operator has a corresponding equivalent function. If
there is an operator <op>, then there is also the
corresponding function operator<op>, with the same
number of arguments and the same type.
The following example shows how we can use the functional syntax of
operators to partially apply the operator ‘+’:
{#
operator+( _, 'b' )( 'a' ),
operator+( 'a', _ )( 'b' ),
apply( operator+( _, 5 ), 10 ),
apply( operator+( _, 5 ), _ )( 10 ),
apply2( operator+, _, _ )( 5, 10 ),
apply2( _, 5 ,10 )( operator+ )
#}
where{
apply( f, x ) = f( x );
apply2( f, x, y ) = f( x, y );
}{# 'ab', 'ab', 15, 15, 15, 15 #}
If an operator is available in both unary and binary form, then the
corresponding operator<op> function only corresponds
to the binary form. For example, while the operator ‘-’ is
available in both unary and binary form, the corresponding
operator- function has only the binary form. It is the same
for ‘*’ and ‘%’ operators.
Due to the functional semantics of subroutines, the source code of programs written in functional programming languages is very often quite unreadable.
For example, consider the following definition of the function
g. For a given string s, it extracts a
substring starting at position 5 of length 8, then replaces each
‘a’ with ‘B’, then replaces each
‘b’ with ‘A’, and finally converts the string
to lowercase. Basically, it is a very simple function, but it is very
unreadable, no matter how carefully we indent the code:
g(s) =
strLowerCase(
strReplaceAll(
strReplaceAll(
subStr(s,5,8),
'a','B'
),
'b','A'
)
);Wafl tries to solve the problem by introducing a dot operator
., which is very similar to the object-oriented dot
syntax. In short, any function that is defined for at least one
argument can be used with more than just the usual syntax:
f( x1, x2, ..., xn)
but also using the dot syntax, where the function application is written as an OO method of the first argument:
x1.f( x2, ..., xn)
These two syntaxes have the same semantics. Programmers are free to use both and choose the best one for each case.
Using the dot syntax usually leads to better readable source code. In
the following example, we define two equivalent functions. For
f we use the dot syntax and for g we
use the usual syntax of the function application.
{#
f( 'abcdABCDabcdABCD' ),
g( 'abcdABCDabcdABCD' )
#}
where{
f(s) =
s.subStr(5,8)
.strReplaceAll('a','B')
.strReplaceAll('b','A')
.strLowerCase();
g(s) =
strLowerCase(
strReplaceAll(
strReplaceAll(
subStr(s,5,8),
'a','B'
),
'b','A'
)
);
}{# 'bcdbacda', 'bcdbacda' #}
If the dot operator is used after an integer literal, the parser may
treat the dot symbol ‘.’ as a decimal point. To avoid
confusion in such cases, simply insert a space before the dot symbol.
For example, use 2 .abs() instead of
2.abs().
A consecutive use of dot syntax can represent an application of transofrmational semantics. Each application of the dot syntax represents a single transformation step that transforms its first argument into the result of the applied function. The entire sequence applications of the dot operator represents a series of such transformation steps.
In the last example, the function f is defined as a
series of transformations of the input string x. Four
successive transformations are applied to calculate the result of the
function.
To enable such applications, in view of the dot syntax, any function that has a main argument and can be regarded as a kind of transformation of this argument, should be defined so that the main argument is the first one.
The dot operator has the highest priority. The only operators with
higher precedence are the library referencing operator
‘::’, the record/tuple element selector ‘$’
and the record/tuple element update operator ‘^’. In the
following example, the abs function is used via the dot
operator before the multiplication:
-3 * 2 .abs()-6
The high precedence of the dot operator often requires the use of
parentheses around the preceding expression. To avoid annoying
parentheses, Wafl supports the arrow operator
‘=>’, which behaves very similarly to the dot operator,
with the only difference that it has the lowest precedence.
If we replace ‘.’ with ‘=>’ in the
previous example, then the application of the function abs
has the lowest priority and is only evaluated after the entire previous
expression has been evaluated:
-3 * 2 => abs()6
The lowest precedence means that there is no other operator or syntax
construction with a higher priority. For example, the function
echoLn in the next example is not only applied to the
preceding else expression, but to the entire
if-then-else expression:
if 3>2
then 3
else 2
=> echoLn()
+ 13
4
In the first Wafl versions, this technique was called arrow
syntax and the operator ‘->’ was used instead of
‘.’. In the current version, the operators
‘->’ and ‘.’ are equivalent. However, we
recommend using the dot syntax. The arrow operator may no longer be
supported in some of the future versions. This old arrow operator
‘->’ should not be confused with the new arrow operator
‘=>’.
A few sections earlier, there was an example of Fibonacci numbers:
{# fib(1), fib(2), fib(3), fib(4), fib(5), fib(6), fib(7) #}
where{
fib( n ) =
if n<=2 then 1
else fib(n-1) + fib(n-2);
}{# 1, 1, 2, 3, 5, 8, 13 #}
This is an interesting example of recursive programming. However,
even a simple analysis shows that this approach is very inefficient. The
problem is that the function is evaluated over and over again for the
same numbers. Let us analyze the case of fib(6):
fib(6)
-> fib(5) + fib(4)
| -> fib(3) + fib(2)
| | -> 1
| -> fib(2) + fib(1)
| -> 1 -> 1
-> fib(4) + fib(3)
| -> fib(2) + fib(1)
| -> 1 -> 1
-> fib(3) + fib(2)
| -> 1
-> fib(2) + fib(1)
-> 1 -> 1
As we can see: fib(6) is evaluated once,
fib(5) once, fib(4) twice, fib(3)
three times, fib(2) 5 times and fib(1) 3
times. We see that this sequence corresponds to the Fibonacci number
sequence, except for the last step: 1, 1, 2, 3, 5, 3. Since only calls
to the lowest level return the exact value, it is easy to conclude that
for any n the total number of evaluations of
fib(2) and fib(1) is equal to
fib(n). And this grows rapidly, approximately with ϕn, where ϕ is the golden ratio
(1.61803…).
So our function fib becomes very slow for large numbers.
How can we improve this? The formula shows that we should keep the last
two results during the computation. This is relatively easy to do in
imperative programming. In C/C++, for example, we can do something like
the following:
unsigned fib( unsigned n ) {
unsigned prev2=1, prev1=1, result=1;
// for n=0 and n=1 result is already ok
// result == 1 == fib(0) == fib(1)
// prev1 == 1 == fib(1)
// prev2 == 1 == fib(0)
for( int i=2; i<=n; i++ ) {
result = prev1 + prev2; // result = fib(i)
prev2 = prev1; // prev2 = fib(i-2)
prev1 = result; // prev1 = fib(i-1)
}
return result;
}But in Wafl we do not have variables, so it’s not as simple as in C++. There is no implicit computation state, and the only way to pass the computation state is to do it explicitly. One solution could be to pass the same state as in the C++ example with every recursive call:
{# fib(1), fib(2), fib(3), fib(4), fib(5), fib(6), fib(7) #}
where {
fib(n) = if n <= 2 then 1
else fib_s(1,1,2,n);
fib_s(p1,p2,i,n) = if i=n then p2
else fib_s(p2,p1+p2,i+1,n);
}{# 1, 1, 2, 3, 5, 8, 13 #}
If we check this carefully, we can remove the constant factor
n from fib_s and get a better solution:
{# fib(1), fib(2), fib(3), fib(4), fib(5), fib(6), fib(7) #}
where {
fib(n) = if n <= 2 then 1
else fib_s(1,1,n-2);
fib_s(p1,p2,i) = if i=0 then p2
else fib_s(p2,p1+p2,i-1);
}{# 1, 1, 2, 3, 5, 8, 13 #}
However, it is not hard to see that this approach to the problem is not so nice.
Another approach to exponentially growing recursion is to memorize the previous results. Cached functions can help here.
Cached functions is a Wafl technique to memorize the previous results
of a function. If we have a fast growing recursion tree, as in the case
of fib, or if we have a function that is computed very
often for the same arguments, then we can suggest that this function
should have its results cached.
The syntax is simple:
<name>(<args>) = cached <exp>;
If we try to use it with fib, then it becomes:
{# fib(1), fib(2), fib(3), fib(4), fib(5), fib(6), fib(7) #}
where{
fib( n ) = cached
if n<=2 then 1
else fib(n-1) + fib(n-2);
}{# 1, 1, 2, 3, 5, 8, 13 #}
When we test this solution with larger numbers, we see that it is
much faster than the original solution, but that the previous solution
with explicit state transfer is still more efficient. This is no
surprise - the problem is that the cached solution has to store the
computed results and search for the previously stored result. However,
if we apply fib over and over again to the same range of
numbers, the cached solution becomes much more efficient than any
other.
And it is easy to use, which is a big advantage.
The efficiency of caching depends largely on the complexity of the arguments and the results. If they are complex (require a large amount of memory or are expensive to compare), then the benefit is questionable.
Wafl internally tries to reduce the size of the memory for the cached results to avoid high memory consumption, by removing some cached values. However, the memory consumption can become considerable if many functions are cached or if the range of arguments is very large, which is often the case for the functions with many arguments.
Wafl has four primitive types: Integer,
Float, String and Bool. In this
chapter we discuss the important elements of the primitive types.
This chapter is quite long and detailed, but its content is elementary, so we recommend a cursory reading. It is enough to get an idea of what is supported, and later you can use this chapter as a reference if needed.
Integer and float literal constants have the same syntax as in the programming languages C and C++.
Floating point literals must contain a decimal point and at least one digit in front of it.
Logical literal constants are true and
false.
String literals are quoted using single or double quotation marks. It does not matter which type of quotation mark is used, but the same type must be used at the beginning and at the end of the string.
Special characters are specified by escape sequences, like in C/C++.
The most important escape sequences are: single quotation mark
(\'), double quotation mark (\"), backslash
(\\), new line (\n), carriage return
(\r), horizontal tab (\t), vertical tab
(\v), form feed (\f) and backspace
(\b). As in C/C++, the characters can be encoded with 3
octal digits: \nnn.
Examples of integer literals:
{#
0, 42, -21
#}{# 0, 42, -21 #}
Examples of float literals:
{#
3.4, 0., 1.2e-3
#}{# 3.4, 0., 0.0012 #}
Examples of bool literals:
{#
true, false
#}{# true, false #}
Examples of string literals:
{#
'single quotation marks',
"double quotation marks",
"two\nlines",
"octal codes A=\101 a=\141"
#}{# 'single quotation marks', 'double quotation marks', 'two\012lines', 'octal codes A=A a=a' #}
Wafl has the usual arithmetic operators:
+);-);*);/);%);%%);**) and-).The division of integer values always produces an integer result.
The remainder and modulus operators are very similar. The difference
is that the remainder of the integer division (%) returns
positive values for positive dividends and negative values for negative
dividends, while the modulus operator (%%) always returns a
positive result:
{#
17 / 10,
17 % 10,
17 %% 10,
-17 / 10,
-17 % 10,
-17 %% 10,
17 / -10,
17 % -10,
17 %% -10,
-17 / -10,
-17 % -10,
-17 %% -10
#}{# 1, 7, 7, -1, -7, 3, -1, 7, 7, 1, -7, 3 #}
Bit-level integer operators are syntactically and semantically equivalent to these operators in C/C++:
&);|);<<);>>) and~).{#
// '11110000' & '00111111' = '00110000' = 48
240 & 63,
// '011' | '110' = '111' = 7
3 | 6,
// bit-level complement
~5,
// '11110' << 3 = '11110000' = 240
30 << 3,
// '11111111' >> 3 = '11111' = 31
255 >> 3
#}{# 48, 7, -6, 240, 31 #}
The power operator a ** b returns a to the
power of b. Any integer raised to a negative power will
yield zero, except for one. One raised to any power will always yield
1:
{#
2 ** 3,
2 ** -3,
1 ** 3,
1 ** -3,
-2 ** 3,
-2 ** -3
#}{# 8, 0, 1, 1, -8, 0 #}
Multiplication, division, remainder, modulus and bit-level conjunction have a higher precedence than addition, subtraction and bit-level disjunction. The power operator has the higher priority. Shift operators have the lower priority.
Wafl has the usual float operators:
+);-);*);/);**) and-).{#
2.1 + 3.45678,
3.0 - 1.2,
3.14 * 2.17,
17.0 / 10.,
2.0 ** 3.0,
2.0 ** 0.5
#}{# 5.55678, 1.8, 6.8138, 1.7, 8., 1.4142135623730951 #}
Wafl has a single binary string operator:
"One" + "Two"OneTwo
There are also indexing operators. They are discussed in the section on the sequence types.
The usual logical operators are supported in both C-like and SQL-like syntax:
&&, and);||, or) and!, not).{#
true or false,
true || false,
true and false,
true && false,
not true,
!true
#}{# true, true, false, false, false, false #}
The usual comparison operators are defined for the types
Integer, Float and String:
==) and
SQL-like (=);!=)
and SQL-like (<>);<);<=);>) and>=).{#
1 < 2,
1.2 <= 2.3,
"abcd" > "ABCD",
"abcd" >= "AB",
21 * 2 = 42,
21 == 42 / 2,
17 != 18,
-3.14 <> 3.14
#}{# true, true, true, true, true, true, true, true #}
Wafl has no variables and no assignments. The operator
‘=’ has only two purposes: (1) to separate the definition
name from the body and (2) as an equality operator. It can never be
ambiguous, so there’s no reason to use the ‘==’ operator
instead, but if someone likes it better, that’s fine.
Wafl is a strongly typed programming language and no implicit type conversions are allowed. Therefore, the Wafl core library contains the conversion functions:
asInt - from any other primitive type to
Integer;asFloat - from any other primitive type to
Float;asString - from any other type to
String;asChar - from any other primitive type to a single
character String andasBool - from any other primitive type to
Bool.There are several other conversion functions for specific type pairs.
It may seem strange to call these functions as..., but
it is quite natural if we assume that we will mainly use them mainly
with the dot syntax.
The function asInt(x) converts every non-integer
primitive value x into the Integer type:
asInt(x) converts a Float value
x to the nearest integer, just like the synonymous function
round;asInt(x) converts a String value
x, which is a valid integer literal, into the corresponding
integer value;
x is a float value literal, only the digits before
decimal point are used;x is not a valid integer literal, asInt
returns zero;asInt(x) converts the Bool values
true to the integer value 1 and
false to 0.For the conversion from Float to Integer,
there are also:
round(x) - the same as asInt(x), converts
a float value into the nearest Integer;ceil(x) - returns the nearest integer that is not
smaller andfloor(x) - returns the nearest integer that is not
larger.There are also functions for converting String values to
Integer:
ascii(x) - converts a string value x to
the ASCII code of the first character of the string;
x is an empty string, the result of the function is
zero.Examples of conversions from Float to
Integer:
{#
asInt(3.6),
asInt(-3.6),
round(3.6),
round(-3.6),
ceil(3.6),
ceil(-3.6),
floor(3.6),
floor(-3.6)
#}{# 4, -4, 4, -4, 4, -3, 3, -4 #}
Examples of conversions from String to
Integer:
{#
asInt('3'),
asInt('3.8'),
asInt('abc'),
ascii('abc'),
ascii('')
#}{# 3, 3, 0, 97, 0 #}
Examples of conversions from Bool to
Integer:
{#
asInt(true),
asInt(false)
#}{# 1, 0 #}
The function asFloat(x) converts every primitive
non-float value x into the type Float:
asFloat(x) converts the Integer value
x to the corresponding Float value.asFloat(x) converts the String value
x, which represents a valid Float literal,
into the corresponding Float value;
x is not a valid Float literal,
asFloat returns zero;asFloat(x) converts the Bool value
true into the value 1.0 and the value
false into 0.0.{#
asFloat(7),
asFloat('6.2'),
asFloat('abc'),
asFloat(true),
asFloat(false)
#}{# 7., 6.2, 0., 1., 0. #}
There are four functions and an operator for converting values of other data types into strings:
Function / Type and Description
asString
('1 -> String)
Converts a value to a string.
asChar
(PrimeNotString['1] -> String)
Converts a value to a character.
toString
(Float * Int -> String)
Converts a float value to a string with given precision.
asPreview
('1 -> String)
Converts a value to a shortened string.
The function asString(x) converts every non-string value
x into String. It converts a value
x into its string representation, according to the Wafl
syntax.
There is a synonymous unary postfix operator $ with the
same behavior.
“Any” means “any” - the function asString and the
postfix operator $ convert any Wafl value
of any type to its String
representation.
{#
asString(3),
asString(3.14),
asString(true),
asString(false),
asString('123'),
asString( {# 1, 2.3, "abc", {# true, 's' #} #} ),
asString([1,2,3,4,5,6,7,8,9,10]),
3$,
3.14$,
true$,
false$,
'123'$,
{# 1, 2.3, "abc", {# true, 's' #} #}$,
[1,2,3,4,5,6,7,8,9,10]$
#}{# '3', '3.14', 'true', 'false', '123', '{# 1, 2.3, \'abc\', {# true, \'s\' #} #}', '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]', '3', '3.14', 'true', 'false', '123', '{# 1, 2.3, \'abc\', {# true, \'s\' #} #}', '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]' #}
The function asPreview(x) is similar to
asString, but returns a shorter string. For simple
data, it behaves in the same way as asString. For larger
structured data and longer strings, it extracts only a part of the
complete string representation.
{#
asPreview('01234567989'),
asPreview(
'01234567890123456789012345678901234567890123456789'
'01234567890123456789012345678901234567890123456789'
),
asPreview([1,2,3,4,5,6,7,8,9,10]),
asPreview(1..1000)
#}{# '01234567989', '012345678901234567890123456789 ... 0123456789 (len=100)', '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]', '[1, 2, 3, 4, 5, ..., 999, 1000] (len=1000)' #}
The function asChar(x) works with integers and logical
values. It:
Integer into a string consisting of a
single character with a given ASCII code;true into the string value
"T" and the logical value false into the
string value "F".{#
asChar(65),
asChar(65.7),
asChar(true),
asChar(false)
#}{# 'A', 'B', 'T', 'F' #}
The function toString(x,n) converts the float value
x into string with n decimal places.
{#
toString(1234.56789,0),
toString(1234.56789,1),
toString(1234.56789,2),
toString(1234.56789,3)
#}{# '1235.', '1234.6', '1234.57', '1234.568' #}
The function asBool(x) converts every primitive non-bool
value x into the type Bool:
asBool(x) converts all non-zero Integer
values to true and zero to false;asBool(x) converts all non-zero Float
values to true and zero to false;asBool(x) converts string values "true"
and "T" to true, and all other values to
false.{#
asBool( 2 ),
asBool( -3 ),
asBool( 0 ),
asBool( 2.1 ),
asBool( -3.2 ),
asBool( 0.0 ),
asBool( "true" ),
asBool( "True" ), // this is not same as "true"
asBool( "T" ),
asBool( "t" ) // this is not same se "T"
#}{# true, true, false, true, true, false, true, false, true, false #}
Wafl core library includes three integer functions:
abs(x) -
absolute value;sgn(x) -
sign andbetween(x,a,b)
- check whether x is between a and
b andrandom(x) -
random value.absThe integer function abs(x) computes an absolute integer
value of the given integer value x:
{#
abs( 123 ),
abs( -123 ),
abs( -0 )
#}{# 123, 123, 0 #}
sgnThe integer function sgn(x) returns a sign of
the number x. For positive values it returns
1, for negative values -1 and for zero it
returns zero:
{#
sgn( 20 ),
sgn( -2 ),
sgn( 0 )
#}{# 1, -1, 0 #}
betweenThe integer function between(x,a,b) checks whether the
number x lies between the numbers a and
b, including the limits. It is usually applied as
x.between(a,b). This is equivalent to the expression
x >= a and x <= b:
{#
10 .between( 5, 20 ),
10 .between( 10, 20 ),
10 .between( 5, 10 ),
10 .between( 5, 9 ),
10 .between( 11, 20 )
#}{# true, true, true, false, false #}
randomThe integer function random(x) returns a random integer
value in the range [0,x-1]. In the following example, we
compute 20 random values in the range [0,4]:
{#
random( 5 ), random( 5 ), random( 5 ), random( 5 ),
random( 5 ), random( 5 ), random( 5 ), random( 5 ),
random( 5 ), random( 5 ), random( 5 ), random( 5 ),
random( 5 ), random( 5 ), random( 5 ), random( 5 ),
random( 5 ), random( 5 ), random( 5 ), random( 5 )
#}{# 0, 1, 3, 3, 2, 1, 2, 4, 0, 3, 2, 1, 4, 0, 0, 0, 3, 1, 4, 2 #}
By default the random number generator is reinitialized the first time it is used, using the current system timer as the seed. This is usually exactly what is expected and required.
However, sometimes it can be necessary to have the same random number
sequence every time the program is executed (for debugging, benchmarking
and some other cases). For such cases there is the clwafl
command line option -nornd, which specifies a fixed
predefined seed initialization.
Execute the previous program with:
clwafl -nornd program.wafl
and it will always return the same result.
If a program uses a parallel evaluation, the order of the randomly
generated numbers is not guaranteed. If the -nornd option
is used, the order of the numbers generated will be the same
but its use by different threads will not give the same results
each time it is run.
The Wafl core library contains the following float functions:
abs(x) -
absolute value;sgn(x) -
sign;between(x,a,b)
- check whether x is between a and
b androundTo(x,y) -
rounding;exp(x) -
e to the power of x;ln(x) - natural
logarithm;log(x) - logarithm
to the base 10;log2(x) -
logarithm to the base 2;pow(x,y) -
x to the power of y;sqrt(x) - square
root;sin(x)
- sine;cos(x)
- cosine;tan(x)
- tangent;asin(x)
- arc sine;acos(x)
- arc cosine;atan(x)
- arc tangent andatan2(y,x) -
arc tangent of y/x (works for
x=0).The following conversion functions have already been presented in the previous sections:
round(x)
- converts a float value to the nearest Integer value;ceil(x) -
returns the nearest Integer value that is not smaller
andfloor(x)
- returns the nearest Integer value that is not
larger.absThe float function abs(x) returns the absolute value of
a given float value x.
{#
abs( 123.456 ),
abs( -123.456 ),
abs( -0.0 )
#}{# 123.456, 123.456, 0. #}
sgnThe float function sgn(x) returns the sign of
the number x. For positive values it returns
1.0, for negative values -1.0 and for zero it
returns zero:
{#
sgn( 20.3 ),
sgn( -2.4 ),
sgn( 0.0 )
#}{# 1., -1., 0. #}
betweenThe float function between(x,a,b) checks whether the
number x lies between the numbers a and
b, including the limit values. It is usually applied as
x.between(a,b). This is equivalent to the expression
x >= a and x <= b:
{#
10.0 .between( 9.5, 10.5 ),
10.0 .between( 10.0, 10.5 ),
10.0 .between( 9.5, 10.0 ),
10.0 .between( 10.1, 10.5 ),
10.0 .between( 9.5, 9.9 )
#}{# true, true, true, false, false #}
roundToThe float function roundTo(x,y) rounds the float value
x. The given float value y defines a least
significant digit.
{#
roundTo( 1234.56789, 0.001 ),
roundTo( 1234.56789, 0.01 ),
roundTo( 1234.56789, 0.1 ),
roundTo( 1234.56789, 1. ),
roundTo( 1234.56789, 10. ),
roundTo( 1234.56789, 100. ),
roundTo( 1234.56789, 1000. ),
roundTo( 1234.56789, 10000. )
#}{# 1234.568, 1234.57, 1234.6000000000001, 1235., 1230., 1200., 1000., 0. #}
exp{#
exp( -10.0 ),
exp( 0.0 ),
exp( 1.0 ),
exp( 10.0 )
#}{# 4.5399929762484854e-05, 1., 2.718281828459045, 22026.465794806718 #}
lnThe float function ln(x) returns the natural logarithm
loge x. It is defined for positive
float values.
{#
ln( 0.1 ),
ln( 1.0 ),
ln( 2.7182818284590452353602874),
ln( 100. ),
ln( 1000. )
#}{# -2.3025850929940455, 0., 1., 4.605170185988092, 6.907755278982137 #}
logThe float function log(x) returns the logarithm
log10 x. It is defined for positive
float values.
{#
log( 0.001 ),
log( 0.01 ),
log( 0.1 ),
log( 1.0 ),
log( 10. ),
log( 100. ),
log( 1000. )
#}{# -3., -2., -1., 0., 1., 2., 3. #}
log2The float function log2(x) returns the logarithm
log2 x. It is defined for positive
float values.
{#
log2( 0.001 ),
log2( 0.0078125 ),
log2( 0.25 ),
log2( 0.5 ),
log2( 1.0 ),
log2( 2. ),
log2( 4. ),
log2( 128. ),
log2( 1000. )
#}{# -9.965784284662087, -7., -2., -1., 0., 1., 2., 7., 9.965784284662087 #}
powThe float function pow(x,y) returns
xy - x to the power of
y.
It is defined for positive x and any y.
Negative x is only permitted if y is an
integer. Zero x is only permitted for positive
y.
{#
pow( 2., 3. ),
pow( 2., -3. ),
pow( 2.5, -3.7 ),
pow( -2., 3. ),
pow( 0., 3.2 )
#}{# 8., 0.125, 0.03369938443095647, -8., 0. #}
sqrtThe float function sqrt(x) returns the square root of
x. It is defined for non-negative float values
x.
{#
sqrt(1.),
sqrt(4.),
sqrt(9.),
sqrt(16.),
sqrt(3433.32)
#}{# 1., 2., 3., 4., 58.594538994687895 #}
The following trigonometric functions are available:
sin(x) - sine;cos(x) - cosine;tan(x) - tangent;asin(x) - arc sine;acos(x) - arc cosine;atan(x) - arc tangent andatan2(y,x) - arc tangent of
y/x (works for x=0).The angles are measured in radians. In addition,
atan2(x,y) maps a pair of float values to the corresponding
angle. If y is not zero,
atan2(x,y) = atan(x/y), but atan2 is also
defined for y=0.
{#
sin(3.14/2.0) * cos(3.14/2.0),
tan(3.14/2.0),
asin(0.5) + acos(0.5),
atan(0.5),
atan2(1.0,2.0),
atan2(1.0,0.0)
#}{# 0.0007963264582434141, 1255.7655915007897, 1.5707963267948968, 0.4636476090008061, 0.4636476090008061, 1.5707963267948966 #}
In this section we introduce the string functions.
The conversion functions (asChar, asString,
ascii and toString) are presented in the previous sections.
The Wafl String type works with both single-byte strings and UTF-8 encoded multi-byte strings. However, some of the functions work with single-byte strings only. If a function does not work well with UTF-8 strings, this will be noted in this tutorial.
Function / Type and Description
strLen
(String -> Int)
Returns the length of the character string.
length
(Indexable['1]['2]['3] -> Int)
Returns the size of the collection.
size
(Indexable['1]['2]['3] -> Int)
Returns the size of the collection.
strCat
(String * String -> String)
String concatenation. The same as string addition.
isNull
(String -> Bool)
Checks whether a string is a database NULL value.
ifNull
(String * String -> String)
Replaces null with the given value:
ifNull(x,c) = if isNull(x) then c else x
strLenThe function strLen(x) returns the length of the string
x.
It is important to understand that string x can contain
any characters and that characters with the ASCII code zero are not
treated as string terminals. Therefore, the String type can
work not only with character strings, but also with byte strings.
There are two more general synonyms length and
size.
{#
strLen( "abc" ),
strLen( "abc\0abc" ),
length( "abc\0abc" ),
size( "abc\0abc" )
#}{# 3, 7, 7, 7 #}
In the case of UTF-8 strings, strLen,
length and size return the size in bytes. To
get a real UTF-8 string length in UTF-8 code-points, please use
utfLen.
strCatThe strCat(x,y) function computes the concatenation of
two given strings. It is equivalent to the string operator
+.
{#
"abc" + "def",
strCat( "abc", "def" )
#}{# 'abcdef', 'abcdef' #}
isNull, ifNullDue to the databases, the String type supports the
special undefined value NULL. The
isNull(s) function checks whether the string s
is NULL. The function ifNull(s,x) returns s if
s is not NULL, but x if s is
NULL.
ifNull(s,x) == if isNull(s) then x else s{#
$-1, // This returns null string
isNull('a'),
isNull($-1),
ifNull("abc","xyz"),
ifNull($-1,"xyz")
#}{# 'NULL', false, true, 'abc', 'xyz' #}
In the previous example, we used the expression $-1 to
generate NULL strings. The prefix operator $
will be introduced later.
String extraction functions extract a part of the given string and return it. Wafl core library contains the following string extraction functions:
Function / Type and Description
sub
(SequenceStr['2]['1] * Int * Int -> SequenceStr['2]['1])
Extracts the subsequence from given 0-based position and given
length: sub(seq,pos,len)
subStr
(String * Int * Int -> String)
Returns a substring from given position (from 0) and with given
length. [Deprecated. Use sub.]
strLeft
(String * Int -> String)
Returns first N characters of the string. If N is negative, returns all but last -N elements.
strRight
(String * Int -> String)
Returns last N characters of the string. If N is negative, returns all but first -N elements.
strLTrim
(String -> String)
Trims all spaces from left side.
strRTrim
(String -> String)
Trims all spaces from right side.
strTrim
(String -> String)
Trims all spaces from both sides.
sub and subStrThe function sub(s,p,n) returns a substring of character
string s that starts at the zero based position
p with the length n.
In the Wafl core library there is subStr, which is a
synonym for sub. In the current version both functions are
supported, but it is possible that only sub remains in
future versions.
{#
subStr( "abcdefgh", 0, 3 ),
sub( "abcdefgh", 0, 3 ),
sub( "abcdefgh", 2, 3 ),
sub( "abcdefgh", -2, 5 ),
sub( "abcdefgh", 5, 10 ),
sub( "abcdefgh", 5, -2 )
#}{# 'abc', 'abc', 'cde', '', 'fgh', '' #}
Special cases:
sub( "abcdefgh", -2, 5 ));sub( "abcdefgh", 5, 10 ))sub( "abcdefgh", 5, -2 )).In the case of UTF-8 strings, sub and
subStr can return invalid strings. These functions treat
strings as if they only consist of single byte characters. If a
substring starts or ends in the middle of a multi-byte UTF-8 code-point,
the result is not a valid UTF-8 string. To obtain a valid UTF-8
substring whose positions are specified in UTF-8 code-points, please use
utfSub.
strLeft and
strRightThe strLeft(s,n) function returns a substring containing
the first n characters of the string s:
n, that is less than
strLen(s), it is the same as sub(s,0,n);n it is the same as
strLeft(s,strLen(s)+n)
The strRight(s,n) function returns a substring
containing the last n characters of the string
s:
n, that is less than
strLen(s), it is the same as
sub(s,strLen(s)-n,n);n it is the same as
strRight(s,strLen(s)+n)
{#
strLeft( "abcdefgh", 3 ), // first 3 characters
strLeft( "abcdefgh", 10 ), // whole string
strLeft( "abcdefgh", -5 ), // all but last 5 characters
strLeft( "abcdefgh", -10 ), // empty string
strRight( "abcdefgh", 3 ), // last 3 characters
strRight( "abcdefgh", 10 ), // whole string
strRight( "abcdefgh", -5 ), // all but first 5 characters
strRight( "abcdefgh", -10 ) // empty string
#}{# 'abc', 'abcdefgh', 'abc', '', 'fgh', 'abcdefgh', 'fgh', '' #}
In the case of UTF-8 strings, strLeft and
strRight can return invalid strings. These functions treat
strings as if they only consist of single byte characters. If a
substring starts or ends in the middle of a multi-byte UTF-8 code-point,
the result is not a valid UTF-8 string. To obtain a valid UTF-8
substring whose positions are specified in UTF-8 code-points, please use
utfLeft and utfRight.
strLTrim,
strRTrim and strTrimThe function strLTrim(s) returns the largest substring
of s that does not contain any leading, non-visible
characters.
The function strRTrim(s) returns the largest substring
of s that does not contain any trailing, non-visible
characters.
The function strTrim(s) returns the largest substring of
s that contains neither leading nor trailing, non-visible
characters.
{#
strLTrim( "\0 \t \n abcd \b \003 \0 \r " ),
strRTrim( "\0 \t \n abcd \b \003 \0 \r " ),
strTrim( "\0 \t \n abcd \b \003 \0 \r " )
#}{# 'abcd \010 \003 \000 \015 ', '\000 \011 \012 abcd', 'abcd' #}
The index operator s[i] is equivalent to
subStr(s, i %% strLen(s) , 1). This means that indexing
beyond the length is possible. The index operator s[i] is
similar, but not equivalent to subStr(s,i,1). They are only
equivalent if the following applies: 0 <= i <
strLen(s)
{#
s[-4], s[-3], s[-2], s[-1],
s[0], s[1], s[2], s[3],
s[4], s[6], s[7], s[8]
#}
where {
s = "abcd";
}{# 'a', 'b', 'c', 'd', 'a', 'b', 'c', 'd', 'a', 'c', 'd', 'a' #}
With UTF-8 strings, the indexing operator can return an invalid
string. It treats strings as if they only consist of single byte
characters. If the index points to a UTF-8 multi-byte code-point
element, the result is not a valid UTF-8 string. To get a valid UTF-8
code-point whose position is specified in UTF-8 code-points, please use
utfAt.
The slice operator uses a similar syntax to the index operator, but
behaves like subStr, strLeft and
strRight. If 0 < n < m
<= strLen(s), then:
s[:n] is the same as strLeft(n), it
extracts the first n characters;s[n:] is the same as strRight(-n)), it
extracts all but first n characters ands[n:m] is the same as
strRight(strLeft(s,m),-n), and the same as
subStr(s,n,m-n).If the index n is negative or greater than
strLen(s), then n %% strLen(s) is used. The
same applies to m.
{#
s[:6],
s[:-2],
s[2:],
s[-6:],
s[2:6],
s[2:-2],
s[-6:6],
s[-6:-2]
#}
where {
s = "abcdefgh";
}{# 'abcdef', 'abcdef', 'cdefgh', 'cdefgh', 'cdef', 'cdef', 'cdef', 'cdef' #}
It is often easier to use the slice operator than extraction functions, but basically they do the same thing.
In case of UTF-8 strings, slice operators may return invalid strings.
These operators treat strings as having single byte characters only. If
a slice begins or ends in the middle of a multi-byte UTF-8 code-point,
the result will not be a valid UTF-8 string. To get a valid UTF-8 slice,
with positions denoted in UTF-8 code-points, please use
utfSlice.
In the case of UTF-8 strings, slice operators can return invalid
strings. They treat strings as if they only consist of single byte
characters. If a slice starts or ends in the middle of a multi-byte
UTF-8 code-point, the result is not a valid UTF-8 string. To obtain a
valid UTF-8 slice whose positions are specified in UTF-8 code-points,
please use utfSlice.
The Wafl core library contains the following string search functions:
Function / Type and Description
strPos
(String * String -> Int)
Finds first position of a substring in the string, or -1 if not found.
strPosI
(String * String -> Int)
Same as strPos, but ignores upper and lower case.
strNextPos
(String * String * Int -> Int)
Finds next position of a substring in the string, after given pos.
strNextPosI
(String * String * Int -> Int)
Same as strNextPos, but ignores upper and lower case.
strLastPos
(String * String -> Int)
Finds last position of a substring in the string, or -1 if not found.
strLastPosI
(String * String -> Int)
Same as strLastPos, but ignores upper and lower case.
strNextLastPos
(String * String * Int -> Int)
Finds next last position of a substring in the string, before given pos.
strNextLastPosI
(String * String * Int -> Int)
Same as strNextLastPosI, but ignores upper and lower case.
strBeg
(String * String -> Bool)
Checks whether the 2nd string is at the beginning of the 1st.
strEnd
(String * String -> Bool)
Checks whether the 2nd string is at the end of the 1st.
strHasSub
(String * String -> Bool)
Checks if the string contain the given substring.
strHasSubI
(String * String -> Bool)
Checks if the string contain the given substring, and ignores upper and lower case.
All search functions return the start position of the second
specified string in the first specified string if it is found, and
-1 if it is not found.
Functions whose names end with ‘I’ are case-insensitive:
strPosI, strNextPosI,
strLastPosI, and strNextLastPosI.
In case-insensitive searches, both strings are first converted to upper case. This can be inefficient for larger strings.
strPos,
strPosI, strNextPos and
strNextPosIThe function strPos(s,p) returns the position of the
first occurrence of the string p in the string
s.
The function strPosI(s,p) returns the position of the
first occurrence of the string p in the string
s, ignoring upper and lower case.
The function strNextPos(s,p,i) returns the position of
the first occurrence of the string p in the string
s after position i.
The function strNextPosI(s,p,i) returns the position of
the first occurrence of the string p in the string
s after position i, ignoring upper and lower
case.
{#
strPos( s, n ), // not found
strPos( s, x ),
strNextPos( s, x, 0 ),
strNextPos( s, x, 3 ),
strNextPos( s, x, 6 ),
strNextPos( s, x, 9 )
#}
where {
s = "abcabcabcab";
x = "ab";
n = "xxx";
};{# -1, 0, 3, 6, 9, -1 #}
These functions work with single characters positions! If a specified position is in the middle of a multi-byte UTF-8 code-points, the result will be an invalid UTF-8 string. Also, the case-insensitive functions do not work well for UTF-8 multibyte characters.
strLastPos,
strLastPosI, strNextLastPos and
strNextLastPosIThe function strLastPos(s,p) returns the position of the
last occurrence of the string p in the string
s.
The function strPosI(s,p) returns the position of the
last occurrence of the string p in the string
s, ignoring upper and lower case.
The function strNextPos(s,p,i) returns the position of
the last occurrence of the string p in the string
s before the position i.
The function strNextPosI(s,p,i) returns the position of
the last occurrence of the string p in the string
s before the position i, ignoring upper and
lower case.
These functions work with single characters positions! If a specified position is in the middle of a multi-byte UTF-8 code-points, the result will be an invalid UTF-8 string. Also, the case-insensitive functions do not work well for UTF-8 multibyte characters.
{#
strLastPos( s, n ), // not existing
strLastPos( s, x ),
strNextLastPos( s, x, 9 ),
strNextLastPos( s, x, 6 ),
strNextLastPos( s, x, 3 ),
strNextLastPos( s, x, 0 )
#}
where {
s = "abcabcabcab";
x = "ab";
n = "xxx";
};{# -1, 9, 6, 3, 0, -1 #}
strBeg and
strEndThe functions strBeg and strEnd check
whether the first string has the second string at the beginning or at
the end:
{#
strBeg( "abcdef", "ab" ),
strBeg( "abcdef", "cd" ),
strBeg( "abcdef", "ef" ),
strEnd( "abcdef", "ab" ),
strEnd( "abcdef", "cd" ),
strEnd( "abcdef", "ef" )
#}{# true, false, false, false, false, true #}
strHasSub and
strHasSubIThe strHasSub(s,ss) function checks whether the string
s contains the substring ss. The
strHasSubI function does the same, but ignores case:
{#
strHasSub( "abcdef", "ab" ),
strHasSub( "abcdef", "bc" ),
strHasSub( "abcdef", "ef" ),
strHasSub( "abcdef", "ac" ),
strHasSub( "abcdef", "AB" ),
strHasSub( "abcdef", "BC" ),
strHasSub( "abcdef", "EF" ),
strHasSub( "abcdef", "AC" )
#}{# true, true, true, false, false, false, false, false #}
{#
strHasSubI( "abcdef", "ab" ),
strHasSubI( "abcdef", "bc" ),
strHasSubI( "abcdef", "ef" ),
strHasSubI( "abcdef", "ac" ),
strHasSubI( "abcdef", "AB" ),
strHasSubI( "abcdef", "BC" ),
strHasSubI( "abcdef", "EF" ),
strHasSubI( "abcdef", "AC" )
#}{# true, true, true, false, true, true, true, false #}
The strHasSub function works well with UTF-8 strings,
but the case-insensitive version strHasSubI does not.
The Wafl core library contains the following fuctions for counting substrings:
Function / Type and Description
strCountSub
(String * String -> Int)
Count occurrences of substring in the given string:
strCountSub('aaaaaA','aa') == 4
strCountSubI
(String * String -> Int)
Same as strCountSub, but ignores upper and lower case:
strCountSub('aaaaaA','aa') == 5
strCountSubDis
(String * String -> Int)
Count disjunct occurrences of substring in the given string:
strCountSub('aaaaaA','aa') == 2
strCountSubDisI
(String * String -> Int)
Same as strCountSubDis, but ignores upper and lower case:
strCountSub('aaaaaA','aa') == 3
All counting functions return the number of occurences of the given substring in the given string. The functions whose names end with ‘I’ ignore the upper and lower case. The functions with ‘Dis’ in the names count only the disjunctive substrings.
{#
strCountSub('aaaaaA','aa'),
strCountSubI('aaaaaA','aa'),
strCountSubDis('aaaaaA','aa'),
strCountSubDisI('aaaaaA','aa')
#}{# 4, 5, 2, 3 #}
The functions strCountSubI and
strCountSubDisI first convert both strings to upper case
letters. They can be inefficient with larger strings.
The Wafl core library contains the following functions for replacing the parts of the strings with another string:
Function / Type and Description
strReplace
(String * String * String * Int -> String)
Replaces Nth occurrence of substring with given string:
strReplace('ababa','b','c',2) == 'abaca'
strReplaceI
(String * String * String * Int -> String)
Same as strReplace, but ignores upper and lower case.
strReplaceAll
(String * String * String -> String)
Replaces all occurrences of substring with given string.
strReplaceAllI
(String * String * String -> String)
Same as strReplaceAll, but ignores upper and lower case.
Each of the strReplace* functions evaluates a new string
and does not change any of the given strings.
The function strReplace(s,p,x,i) returns a copy of
string s in which the i.th occurrence of the
substring p is replaced by x. The string
s remains unchanged.
The function strReplaceI(s,p,x,i) is similar to the
function strReplace, but ignores upper and lower case when
searching for p.
The function strReplaceAll(s,p,x) returns a copy of the
string s in which all occurrences of substring
p are replaced by x. The string s
remains unchanged.
The function strReplaceAllI is similar to the function
strReplaceAll, but ignores upper and lower case when
searching for p.
{#
strReplace( s, 'a', '@', 2 ),
strReplaceI( s, 'a', '@', 2 ),
strReplaceAll( s, 'a', '@' ),
strReplaceAllI( s, 'a', '@' )
#}
where {
s = "abABabAB";
}{# 'abAB@bAB', 'ab@BabAB', '@bAB@bAB', '@b@B@b@B' #}
Please note that the case-insensitive functions
strReplaceI and strReplaceAllI can be
inefficient with larger strings.
strSplit... and strJoinHere we have a list of strings. The list is one of the most important concepts of functional programming languages, including Wafl. We will discuss lists in detail in the following chapter.
The function strSplit(s,p) returns a list of all
substrings of the string s that are separated by the
substring p.
The function strJoin(lst,p) concatenates all elements of
the list lst and inserts the separator p
between them.
Function / Type and Description
strSplit
(String * String -> List[String])
Splits a string to a list of string, by extracting the given separator.
strSplitTrim
(String * String -> List[String])
Splits a string to a list of string, by extracting the given separator. All spaces are trimmed from each segment from left and right side.
strSplitLines
(String -> List[String])
Splits a string to a list of string, by extracting new-line separator.
strSplitLinesTrim
(String -> List[String])
Splits a string to a list of string, by extracting new-line separator. All spaces are trimmed from each segment from left and right side.
strJoin
(Sequence['1][String] * String -> String)
Joins (concatenates) a sequence of strings, adding the given separator.
strSplit( 'a,bb,c,dd,e', ',' )['a', 'bb', 'c', 'dd', 'e']
strJoin( ['a','b','c','d','e','f','g','h'], ';' )a;b;c;d;e;f;g;h
strJoin( strSplit( 'a,bb,c,dd,e', ','), ';' )a;bb;c;dd;e
The function strSplitTrim is similar to
strSplit, but it detects and removes all empty spaces
before and after the separators. It is functionally equivalent, but more
efficient than the mapping of strTrim after the split:
s.strSplit(p).map(strTrim) == s.strSplitTrim(p)The function strSplitLines(s) is similar to
strSplit(s,'\n'), but it detects and removes both LF
(Linux, ‘\n’) and CRLF (Windows, ‘\r\n’) new
line sequences:
{#
strSplit( '\nabc\ndef\nghi\n', '\n' ),
strSplit( '\r\nabc\r\ndef\r\nghi\r\n', '\n' ),
strSplitLines( '\nabc\ndef\nghi\n' ),
strSplitLines( '\r\nabc\r\ndef\r\nghi\r\n' )
#}{# ['', 'abc', 'def', 'ghi', ''], ['\015', 'abc\015', 'def\015', 'ghi\015', ''], ['', 'abc', 'def', 'ghi', ''], ['', 'abc', 'def', 'ghi', ''] #}
The function strSplitLinesTrim is similar to
strSplitTrim and strSplitLines. It is
functionally equivalent, but more efficient than mapping
strTrim after using strSplitLines:
s.strSplitLines().map(strTrim) == s.strSplitLinesTrim()The function strChars converts a string into a list of
characters.
{#
strChars( 'abc\ndef' )
#}{# ['a', 'b', 'c', '\012', 'd', 'e', 'f'] #}
In the case of UTF-8 strings, strChars cuts a string
into bytes. To get a valid UTF-8 code-point list, use
utfChars instead.
Sometimes we need to encode a string in a format that follows a
specific syntax. Wafl contains four string encoding functions. All these
functions have the common type: (String -> String).
Function / Type and Description
strEncodeHtml
(String -> String)
Encodes the string for HTML.
strEncodeSql
(String -> String)
Encodes the string for SQL.
strEncodeUri
(String -> String)
Encodes the string for URI.
strEncodeWafl
(String -> String)
Encodes the string for Wafl code.
The function strEncodeHtml(s) returns an encoded string
that can be inserted into HTML. All special characters are replaced by
corresponding HTML escape sequences:
strEncodeHtml( "abc&<>def" )abc&<>def
The function strEncodeSql(s) returns an encoded string
ready for use in SQL string literals. All special characters are
replaced by SQL escape sequences:
strEncodeSql( "abc'quotes'abc" )abc''quotes''abc
The function strEncodeUri(s) returns an encoded string
according to the rules of URI syntax:
strEncodeUri( "a + b = c" )a%20%2B%20b%20%3D%20c
The function strEncodeWafl(s) returns an encoded string
according to the Wafl syntax:
strEncodeWafl( "a\n \0 \'\"..." )a\012 \000 \'\"...
Here we discuss three other string functions:
Function / Type and Description
strLowerCase
(String -> String)
Converts all letters to lower case.
strUpperCase
(String -> String)
Converts all letters to upper case.
strReverse
(String -> String)
Reverses the string.
The function strLowerCase converts all letters in a
string to lower case.
The function strUpperCase converts all letters in a
string to upper case.
{#
strLowerCase( 'aAbBcC' ),
strUpperCase( 'aAbBcC' )
#}{# 'aabbcc', 'AABBCC' #}
The function strReverse returns the reversed string.
{#
strReverse( 'aAbBcC' )
#}{# 'CcBbAa' #}
These functions do not work well with UTF-8 strings.
To handle UTF-8 strings correctly, please only use the functions that handle strings as sequences of UTF-8 multibyte code-points. Many of the usual string functions work well with UTF-8, also.
Please note that two important functionalities are not yet supported by the Wafl library:
Most of the specific UTF-8 behavior is covered by the following functions.
Some applications require that files with UTF-8 content have a UTF-8 BOM (Byte Order Mark) sequence at the beginning of the file. The following functions enable the handling of UTF-8 BOM.
Please note that the use of UTF-8 BOM is not recommended, as the byte-order in UTF-8 format is irrelevant. It is based on individual bytes, not words.
Function / Type and Description
utfBom
( -> String)
Returns UTF-8 BOM sequence.
utfIsBom
(String -> Bool)
Checks whether a string content is UTF-8 BOM.
utfHasBom
(String -> Bool)
Checks whether a string begins with UTF-8 BOM.
utfAddBom
(String -> String)
Adds a UTF-8 BOM, if not already present.
utfTrimBom
(String -> String)
Trims leading BOM, if present.
The function utfBom() returns the UTF-8 BOM.
The function utfIsBom(s) checks whether the string
content corresponds exactly to the UTF-8 BOM.
The function utfHasBom(s) checks whether the string
begins with a UTF-8 BOM.
{#
utfBom(),
utfIsBom( utfBom() ),
utfIsBom( 'abc' ),
utfHasBom( utfBom() + 'abc' ),
utfHasBom( 'abc' )
#}{# '\357\273\277', true, false, true, false #}
The function utfAddBom(s) returns a string with a UTF-8
BOM appended to the beginning, if it is not already present.
The function utfTrimBom(s) returns a string without a
leading UTF-8 BOM.
{#
utfAddBom('abc'),
utfHasBom( utfAddBom('abc') ),
utfTrimBom( utfAddBom('abc') ),
utfHasBom( utfTrimBom( utfAddBom('abc') ) )
#}{# '\357\273\277abc', true, 'abc', false #}
Function / Type and Description
utfIsValid
(String -> Bool)
Checks whether a string is a valid UTF-8 encoded string.
utfRepInvalid
(String * String -> String)
Replaces invalide code points with the given character.
The function utfIsValid(s) checks whether a string is a
valid UTF-8 string. Please note that each single-byte characters string
is a valid UTF-8 string.
The function utfRepInvalid(s,c) returns a strings in
which all invalid UTF-8 code-points are replaced by the character
c.
{#
utfIsValid( sub('abc€def',4,4) ),
utfRepInvalid( sub('abc€def',0,4), '@' ), // only the first byte of a MB
utfRepInvalid( sub('abc€def',4,4), '@' ) // only the second byte of a MB
#}{# false, 'abc@', '@@de' #}
utfLenFunction / Type and Description
utfLen
(String -> Int)
Returns UTF-8 length, as a number of complete code points.
The function utfLen(s) returns the string length by
counting the complete UTF-8 code-points. The string length in
code-points is always less than or equal to the length in bytes
(strLen, length or size).
{#
strLen( 'abc€def' ),
utfLen( 'abc€def' )
#}{# 9, 7 #}
utfLessThan and
sortUtf8Function / Type and Description
utfLessThan
(String * String -> Bool)
String less than operator replacement for UTF-8.
sortUtf8
(Sequence['1][String] -> Sequence['1][String])
Sorts string sequence by UTF-8 table.
The regular comparison operators work for single-byte strings only.
The function utfLessThan(s1,s2) uses the UTF-8 multi-byte
collation, as implemented for the operating system.
The sortUtf8 function is a equivalent to
sort(_,utfLessThan).
utfAtFunction / Type and Description
utfAt
(String * Int -> String)
Returns a code point at given position, indexed by codepoints.
The function utfAt(s,i) is similar to the indexing
operator s[i], but uses indices based on code-points. It
returns the i-th UTF-8 code-point of the string
s.
For more details, see indexing operator.
{#
s[0], s[1], s[2], s[3],
s.utfAt(0), s.utfAt(1), s.utfAt(2), s.utfAt(3)
#}
where {
s = "abАБабAB";
}{# 'a', 'b', '\320', '\220', 'a', 'b', '\320\220', '\320\221' #}
{#
s[-1], s[-2], s[-3], s[-4],
s.utfAt(-1), s.utfAt(-2), s.utfAt(-3), s.utfAt(-4)
#}
where {
s = "abАБабAB";
}{# 'B', 'A', '\261', '\320', 'B', 'A', '\320\261', '\320\260' #}
utfSub,
utfSlice, utfLeft, utfRightFunction / Type and Description
utfSub
(String * Int * Int -> String)
Returns a substring from given position (from 0) and with given length, indexing complete UTF-8 code points instead of characters.
utfSlice
(String * Int * Int -> String)
Returns a substring between two given positions, indexing complete UTF-8 code points instead of characters.
utfLeft
(String * Int -> String)
Returns first N UTF-8 code points of the string.
utfRight
(String * Int -> String)
Returns last N UTF-8 code points of the string.
The function utfSub(s,p,n) is similar to
subStr(s,p,n), but uses code-point based indices. It
returns a substring of the string s, beginning at the zero
based position p with length n, where position
and length are counted based on UTF-8 code-points instead of
characters.
For more details, please read subStr.
{#
subStr( "abcdАБВГабвгABCD", 0, 8 ),
utfSub( "abcdАБВГабвгABCD", 0, 8 )
#}{# 'abcd\320\220\320\221', 'abcd\320\220\320\221\320\222\320\223' #}
The function utfSlice(s,n,m) is similar to the string
slice operator s[n:m], but uses code-point based indices.
It returns a substring of the string s, starting at the
zero based position n and ending before the position
m, where positions n and m are
counted based on UTF-8 code-points instead of characters.
For more details, see string slice operator.
{#
"abcdАБВГабвгABCD" [2:6],
utfSlice( "abcdАБВГабвгABCD", 2, 6 )
#}{# 'cd\320\220', 'cd\320\220\320\221' #}
The function utfLeft(s,n) is similar to the string
function strLeft, but uses code-point based indices. It
returns a substring containing the first n UTF-8
code-points of the string s.
For more details please study strLeft.
{#
strLeft( "abcdАБВГабвгABCD", 6 ),
utfLeft( "abcdАБВГабвгABCD", 6 )
#}{# 'abcd\320\220', 'abcd\320\220\320\221' #}
The function utfRight(s,n) is similar to string function
strRight, but uses indices based on code-points. It returns
a substring containing the last n UTF-8 code-points of the
string s.
For more details, please see strRight.
{#
strRight( "abcdАБВГабвгABCD", 6 ),
strRight( "abcdАБВГабвгABCD", 6 )
#}{# '\320\263ABCD', '\320\263ABCD' #}
Function / Type and Description
utfChars
(String -> List[String])
Splits a string to a list of UTF-8 code points.
utfReverse
(String -> String)
Reverses UTF-8 string.
The function utfChars(s) is similar to the string
function strChars, but uses code-points instead of
characters. It returns a list of all UTF-8 code-points of the string
s.
For more details please study strChars.
{#
strChars( "abАБабAB" ),
utfChars( "abАБабAB" )
#}{# ['a', 'b', '\320', '\220', '\320', '\221', '\320', '\260', '\320', '\261', 'A', 'B'], ['a', 'b', '\320\220', '\320\221', '\320\260', '\320\261', 'A', 'B'] #}
The function utfReverse(s) is similar to the string
function strReverse, but uses code-points instead of
characters. It returns a reversed string, taking care to preserve the
UTF-8 code-points.
For more details, please read strReverse.
{#
strReverse( "abАБабAB" ),
utfReverse( "abАБабAB" )
#}{# 'BA\261\320\260\320\221\320\220\320ba', 'BA\320\261\320\260\320\221\320\220ba' #}
Many of the regular string functions work well with UTF-8 strings, as long as the arguments are valid UTF-8 strings:
Function / Type and Description
isNull
(String -> Bool)
Checks whether a string is a database NULL value.
ifNull
(String * String -> String)
Replaces null with the given value:
ifNull(x,c) = if isNull(x) then c else x
strCat
(String * String -> String)
String concatenation. The same as string addition.
strLTrim
(String -> String)
Trims all spaces from left side.
strRTrim
(String -> String)
Trims all spaces from right side.
strTrim
(String -> String)
Trims all spaces from both sides.
strBeg
(String * String -> Bool)
Checks whether the 2nd string is at the beginning of the 1st.
strEnd
(String * String -> Bool)
Checks whether the 2nd string is at the end of the 1st.
strHasSub
(String * String -> Bool)
Checks if the string contain the given substring.
strCountSub
(String * String -> Int)
Count occurrences of substring in the given string:
strCountSub('aaaaaA','aa') == 4
strCountSubDis
(String * String -> Int)
Count disjunct occurrences of substring in the given string:
strCountSub('aaaaaA','aa') == 2
strReplace
(String * String * String * Int -> String)
Replaces Nth occurrence of substring with given string:
strReplace('ababa','b','c',2) == 'abaca'
strReplaceAll
(String * String * String -> String)
Replaces all occurrences of substring with given string.
strSplit
(String * String -> List[String])
Splits a string to a list of string, by extracting the given separator.
strSplitTrim
(String * String -> List[String])
Splits a string to a list of string, by extracting the given separator. All spaces are trimmed from each segment from left and right side.
strSplitLines
(String -> List[String])
Splits a string to a list of string, by extracting new-line separator.
strSplitLinesTrim
(String -> List[String])
Splits a string to a list of string, by extracting new-line separator. All spaces are trimmed from each segment from left and right side.
strJoin
(Sequence['1][String] * String -> String)
Joins (concatenates) a sequence of strings, adding the given separator.
strEncodeHtml
(String -> String)
Encodes the string for HTML.
strEncodeSql
(String -> String)
Encodes the string for SQL.
strEncodeWafl
(String -> String)
Encodes the string for Wafl code.
List is the most important and most frequently used structured data type in functional programming languages. List is a sequential type, which means that a list can contain many elements in an ordered sequence. In Wafl, a list can consist of elements of any type, but all elements of a list must have the same type.
Due to the importance of the concept of list processing, it is recommended to study this chapter carefully. The functions discussed here are essential for programming in Wafl.
The general list type is written as List['1], where
'1 denotes a type variable (a pure polymorphic
type). Each specific list in a program must have a specific type,
where this free type is fully defined.
A list without elements is called an empty list. An empty
list is written as nil or []. These two
syntaxes are equivalent.
Non-empty list literals are written as elements enclosed in square brackets and separated by commas.
{#
[],
nil,
[1,2,3], // List[Int]
['a','b','c','d'], // List[String]
[[1,2],[3,4,5],[6,7,8]] // List[List[Int]]
#}{# [], [], [1, 2, 3], ['a', 'b', 'c', 'd'], [[1, 2], [3, 4, 5], [6, 7, 8]] #}
In the previous example we have the following lists:
[];nil;It is allowed to specify a comma separator after the last element of a list. It has no function there, but it makes it easier to edit, copy, and comment/uncomment list elements.
The processing of list elements is often explained using the terms head and tail of the list. The head stands for the first element of the list and the tail stands for all but the first element of the list. Both head and tail only make sense for non-empty lists.
The basic list functions include elementary operations to check the list size, to extract its head, tail or a sub-list, to construct a list and to compare two lists.
Function / Type and Description
A == B
('1 * '1 -> Bool)
Equal-to operator.
A != B
('1 * '1 -> Bool)
Not-equal-to operator.
empty
(Indexable['1]['2]['3] -> Bool)
Checks whether the collection is empty.
nonEmpty
(Indexable['1]['2]['3] -> Bool)
Checks whether the collection is non-empty.
size
(Indexable['1]['2]['3] -> Int)
Returns the size of the collection.
length
(Indexable['1]['2]['3] -> Int)
Returns the size of the collection.
longerThan
(SequenceStr['1]['2] * Int -> Bool)
Checks whether the sequence is longer than the given integer.
hd
(List['1] -> '1)
Extracts the list head. Not defined for empty list.
tl
(List['1] -> List['1])
Extracts the list tail.
A : B
('1 * List['1] -> List['1])
Operator that constructs a new list from the given head and tail.
It may be strange to see a type like
Indexable['1]['2]['3], but there is nothing special about
it. It is one of the indexable collections (where
'1 can be one of the basic indexable collections, such as
list, array, map and string) with indices of type '2
and elements of type '3. This type can therefore be
reduced to List['3].
We could replace the types in the tables, but it is good to adapt to the notation.
Lists can be compared for equality. The operators == and
!= evaluate the expected results:
{#
[] = nil,
[] != nil,
[] = [1,2,3],
[] != [1,2,3],
[1,2,3] = [1,2,3],
[1,2,3] = [1,2,3,4,5],
[1,2,3] != [1,2,3],
[1,2,3] != [1,2,3,4,5]
#}{# true, false, false, true, true, false, false, true #}
empty and
nonEmptyTo check whether a list is empty or not, the functions
empty(lst) and nonEmpty(lst) are available.
They are equivalent to comparing a list with an empty list literal.
{#
empty( [] ),
empty( nil ),
empty( [1,2,3] ),
[] = nil,
[1,2,3] = nil
#}{# true, true, false, true, false #}
{#
nonEmpty( [] ),
nonEmpty( nil ),
nonEmpty( [1,2,3] ),
[] != nil,
[123] != nil
#}{# false, false, true, false, true #}
size,
length and longerThanThe function length(l) computes the length of the list.
The function size(l) is a synonym.
It is planned to keep both in the future. size is more
general, but length is more natural for sequences.
{#
length( [] ),
length( nil ),
length( [1,2,3] ),
size( [1,2,3] )
#}{# 0, 0, 3, 3 #}
In some cases, it is not efficient to compute the length of a list.
For example, if a list is a result of a database query, the length can
only be computed once the complete result has been retrieved from the
database. In such cases, it is often useful to only check whether the
length of the list is greater than a number. The function
longerThan(lst,n) checks whether the list lst
is longer than n and returns true if it
is.
{#
longerThan( [1,2,3], 0 ),
longerThan( [1,2,3], 1 ),
longerThan( [1,2,3], 2 ),
longerThan( [1,2,3], 3 ),
longerThan( [1,2,3], 4 ),
longerThan( [1,2,3], 5 )
#}{# true, true, true, false, false, false #}
hd, tl and
A:BThe function hd(l) returns the first element of the
given non-empty list. It is not defined for empty lists.
The function tl(l) computes the tail of the given
non-empty list. The list tail is a list suffix consisting of
all list elements except the first one. When applied to an empty list,
it returns the same empty list.
The binary infix operator h:l constructs a list with
head h and tail t. The type of the operator is
('1 * List['1] -> List['1]). It is often referred to as
the cons-operator, as many functional programming languages
have a function cons (constructs a list) that has
the same behavior as this operator.
{#
hd( [1,2,3] ),
tl( [1,2,3] ),
1:[2,3],
1:2:3:nil,
[2,3]:nil
#}{# 1, [2, 3], [1, 2, 3], [1, 2, 3], [[2, 3]] #}
List elements are usually processed using recursion:
We will try to apply this concept in some examples.
Let us sum the elements of an integer list by applying the presented concept:
[1,2,3,4,5].sum()
where {
sum( lst ) =
if lst.empty() then 0
else lst.hd() + lst.tl().sum();
}15
Let us compute a list of squares of natural numbers from 1 to 10. We
can start from a list [1,2,...,10] and use a recursive
function squares:
squares of the empty list is an empty list;squares of the tail.[1,2,3,4,5,6,7,8,9,10].squares()
where {
squares( lst ) =
if lst.empty() then []
else lst.hd() * lst.hd() : lst.tl().squares();
}[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
The same result may be computed based on the same idea, but without explicitly writing a list literal:
squares(1,10)
where {
squares( from, to ) =
if from > to
then []
else from * from : squares( from + 1, to );
}[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
As in the previous example, it is often useful to have a list of
integers in a given range. In this example the function
mklst(n,m) computes the list of integers from
n to m:
mklst(100,110)
where{
mklst(n,m) =
if n>m then []
else n : mklst( n+1,m);
}[100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110]
In the examples of the previous section, we can recognize a certain repetitiveness. Let us focus on two topics:
It is common to approach these two topics with two different
functions. First, we write the function processElements
which processes each of the list elements and returns the results in a
new list in the same order. Then we write the function
combineElements, which combines the elements of the result
list as needed.
The function processElements should have two arguments:
the list of elements to be processed and the processing function. This
function is similar to the squares function previously
presented. So let us test it by applying it to the function
\x:x*x:
[1,2,3,4,5].processElements( \x: x*x )
where {
processElements( lst, fun ) =
if lst.empty() then []
else lst.hd().fun() : lst.tl().processElements( fun );
}[1, 4, 9, 16, 25]
The function combineElements should have three
arguments: the list of elements to be combined, the function that
describes the operation of combining and the start value. Let us test it
on summation - the function is the operator+ and the start
value is zero:
[1,2,3,4,5].combineElements( operator+, 0 )
where {
combineElements( lst, fun, value ) =
if lst.empty() then value
else lst.hd().fun( lst.tl().combineElements( fun, value ) );
}15
Now we can use both functions to sum the squares of the numbers in a given list:
[1,2,3,4,5]
.processElements( \x: x*x )
.combineElements( operator+, 0 )
where {
processElements( lst, fun ) =
if lst.empty() then []
else lst.hd().fun() : lst.tl().processElements( fun );
combineElements( lst, fun, value ) =
if lst.empty() then value
else lst.hd().fun( lst.tl().combineElements( fun, value ) );
}55
As we can see, these two functions are very helpful. For this reason,
the equivalents of these two functions as well as many other higher
order functions are implemented in the Wafl core library. The core
library function map is equivalent to
processElements, and foldr is equivalent to
combineElements.
Some basic list functions were introduces in the previous section. Here we introduce some more complex list functions before we look at higher order functions in the following sections.
The type SequenceStr['2]['1] includes sequences and
strings. Sequences are indexable types with integer indices.
sub, subListThe function subList(l,p,n) returns a sublist of the
given list, starting at position p and with n
elements.
Function / Type and Description
sub
(SequenceStr['2]['1] * Int * Int -> SequenceStr['2]['1])
Extracts the subsequence from given 0-based position and given
length: sub(seq,pos,len)
subList
(List['1] * Int * Int -> List['1])
Extracts the sub-list from given 0-based position and given length:
subList(list,pos,len) [Deprecated. Use
sub.]
It has the same semantics as the already introduced sub
and subStr for strings. In addition, sub also
works for lists.
{#
sub( [0,1,2,3,4,5,6,7], 0, 3 ),
sub( [0,1,2,3,4,5,6,7], 2, 3 ),
sub( [0,1,2,3,4,5,6,7], -2, 5 ),
sub( [0,1,2,3,4,5,6,7], 5, 10 ),
sub( [0,1,2,3,4,5,6,7], 5, -2 )
#}{# [0, 1, 2], [2, 3, 4], [], [5, 6, 7], [] #}
Function / Type and Description
A[ : B ]
(SequenceStr['2]['1] * Int -> SequenceStr['2]['1])
Extracts a prefix of the sequence with N elements:
seq[:N]
A[ B : ]
(SequenceStr['2]['1] * Int -> SequenceStr['2]['1])
Extracts a suffix of the sequence with N elements:
seq[N:]
A[ B : C ]
(SequenceStr['2]['1] * Int * Int -> SequenceStr['2]['1])
Extracts a sequence segment from Nth to (M-1)th element:
seq[N:M]
A[ B ]
(Indexable['1]['2]['3] * '2 -> '3)
Extracts an element from the indexable collection:
col[idx]
The index operator lst[i] and the slice operators
lst[:n], lst[:n] and lst[n:m]
have the same semantics as for strings, so we will not go into them in
detail here.
Note that the indexing operator is not defined for empty lists.
{#
lst[-4], lst[-3], lst[-2], lst[-1],
lst[0], lst[1], lst[2], lst[3],
lst[4], lst[6], lst[7], lst[8]
#}
where {
lst = [0,1,2,3];
}{# 0, 1, 2, 3, 0, 1, 2, 3, 0, 2, 3, 0 #}
{#
lst[:6],
lst[:-2],
lst[2:],
lst[-6:],
lst[2:6],
lst[2:-2],
lst[-6:6],
lst[-6:-2]
#}
where {
lst = [1,2,3,4,5,6,7,8];
}{# [1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6], [3, 4, 5, 6, 7, 8], [3, 4, 5, 6, 7, 8], [3, 4, 5, 6], [3, 4, 5, 6], [3, 4, 5, 6], [3, 4, 5, 6] #}
append,
A ++ B and appendAllFunction / Type and Description
A ++ B
(SequenceStr['1]['2] * SequenceStr['1]['2] -> SequenceStr['1]['2])
Appends the the second sequence to the first sequence.
append
(SequenceStr['1]['2] * SequenceStr['1]['2] -> SequenceStr['1]['2])
Appends the second sequence to the first sequence.
appendAll
(Sequence['1][SequenceStr['2]['3]] -> SequenceStr['2]['3])
Appends all sequences in the given sequence.
The function append(l,r) returns a list containing all
elements of the two lists in the given order. The binary infix operator
‘++’ does the same thing.
{#
[1,2,3] ++ [4,5,6],
append( [1,2,3], [4,5,6] )
#}{# [1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6] #}
The function appendAll(l) takes a list of lists and
appends them from left to the right, in a same way as
l.foldl(append,[]) does. All the functional characteristics
of the appendAll are the same as if it would be defined as
appendAll = foldl(_,append,[]). Function
appendAll takes care of internal lists structure and works
more efficient than the foldl based solution. It also works
with lists of arrays, arrays of lists and arrays of arrays.
{#
[[1], [2, 3], [4, 5, 6], [7, 8, 9, 10]].appendAll(),
[[1], [2, 3], [4, 5, 6], [7, 8, 9, 10]].foldl(append,[])
#}{# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] #}
A..B,
intRange, intRangeByFunction / Type and Description
A .. B
(Int * Int -> List[Int])
Returns the list of integers in the range:
2..5 = [2,3,4,5]
intRange
(Int * Int -> List[Int])
Returns the list of integers in the range:
intRange(2,5) = [2,3,4]
intRangeBy
(Int * Int * Int -> List[Int])
Returns the list of integers in the given range with a given step:
intRangeBy(2,10,2) = [2,4,6,8]
intRangeWithLast
(Int * Int -> List[Int])
Returns the list of integers in the range:
intRangeWithLast(2,5) = [2,3,4,5]
The integer range operator n..m computes the list of
integers from n to m, including both limits.
If n < m, the elements will be in the
ascending order, ant if n > m, the elements
will be in the descending order:
{#
0..10,
-5..5,
5..-5
#}{# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5], [5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5] #}
When using the operator A..B, neither parenthesis nor
brackets are required. So, if brackets are used, the [1..2]
will return [[1,2]] and not the expected
[1,2].
The function intRangeWithLast is a synonym for the
operator A..B.
The function intRange(n,m) is similar, but does not
include the end of the range m in the list.
The function intRangeBy(n,m,d) creates a list of
integers from n to m, but uses a step
d instead of 1. As with intRange, its result
does not contain the value m.
{#
5..9,
intRange(5,9),
intRangeBy(5,9,2),
intRangeWithLast(5,9)
#}{# [5, 6, 7, 8, 9], [5, 6, 7, 8], [5, 7], [5, 6, 7, 8, 9] #}
map and
mapIdxFunction / Type and Description
map
(Sequence['2]['1] * ('1 -> '3) -> Sequence['2]['3])
Maps the function to sequence elements: map(seq,fun)
mapIdx
(Sequence['1]['2] * (Int * '2 -> '3) -> Sequence['1]['3])
Maps the function to sequence elements:
mapIdx(seq,fun)
mapThe function map( lst, fun ) is one of the most
important higher order functions. It maps the list lst to a
new list with the same number of elements. Each element x
of the list lst is mapped to the element
fun(x) of the new list, in the same order.
This function is equivalent to our function
processElements from the previous examples.
In the following example, map is used to compute a list
of elements incremented by one:
[1,2,3,4,5,6,7,8,9,10].map( \x: x+1 )[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
The same result:
[1,2,3,4,5,6,7,8,9,10].map( operator+(_,1) )[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
We will use map in many of the following examples.
mapIdxSometimes it can be useful to know at which position in the list the
processed element is located. We can achieve this by transforming
(mapping) a list of values into a list of pairs (index, value) and then
mapping this list of pairs where indexes are available. However, it is
more efficient to have a function mapIdx(lst,fun) where
fun(idx,x) maps the value x at position
idx to a new value.
['a','b','c','d','e','f','g','h']
.mapIdx( \i,x: x + i$ )['a0', 'b1', 'c2', 'd3', 'e4', 'f5', 'g6', 'h7']
zip and
crossFunction / Type and Description
zip
(Sequence['4]['1] * Sequence['4]['2] -> Sequence['4][Tuple['1, '2]])
Zips two sequences to a sequence of pairs.
zipBy
(Sequence['4]['1] * Sequence['4]['2] * ('1 * '2 -> '3)
-> Sequence['4]['3]) Zips two sequences by given
function: zipBy(seq1, seq2 ,fun)
zipByIdx
(Sequence['4]['1] * Sequence['4]['2] * (Int * '1 * '2 -> '3)
-> Sequence['4]['3]) Zips two sequences and indexes
by given function: zipByIdx(seq1, seq2 ,fun)
cross
(Sequence['4]['1] * Sequence['4]['2] -> Sequence['4][Tuple['1, '2]])
Crosses two sequences to a sequence of pairs.
crossBy
(Sequence['4]['1] * Sequence['4]['2] * ('1 * '2 -> '3)
-> Sequence['4]['3]) Crosses two sequences by given
function: crossBy(seq1, seq2 ,fun)
crossByIdx
(Sequence['4]['1] * Sequence['4]['2] * (Int * '1 * Int * '2 -> '3)
-> Sequence['4]['3]) Crosses two sequences and
indexes by given function: crossByIdx(seq1, seq2 ,fun)
zipThe function zip(lst1,lst2) returns a list of tuples,
where each tuple contains the corresponding elements of the two given
lists lst1 and lst2.
zip( [1,2,3], ['A','B','C'])[{# 1, 'A' #}, {# 2, 'B' #}, {# 3, 'C' #}]
zipByThe function zipBy(lst1,lst2,fun) returns a list in
which each element is a result of applying fun(x,y) to the
corresponding elements of the two given lists lst1 and
lst2.
zipBy( ['a','b','c'], ['A','B','C'], operator+ )['aA', 'bB', 'cC']
zipByIdxThe function zipByIdx(lst1,lst2,fun) returns a list
where each element is a result of applying fun(i,x,y) to
the corresponding elements of the two given lists lst1 and
lst2 and their index (position).
zipByIdx( ['a','b','c'], ['A','B','C'],
\i,x,y: i$ + '-' + x + '-' + y
)['0-a-A', '1-b-B', '2-c-C']
crossThe function cross(lst1,lst2) returns a list of all
possible pairs of the elements from lst1 and
lst2. The order of the pairs is the same as in the
lst1 and then lst2, i.e. in the result of
cross([1,2],[3,4]) first 1 will be paired with
3 and 4 and then 2 will be paired
with 3 and 4.
cross( [1,2,3], ['A','B','C'])[{# 1, 'A' #}, {# 1, 'B' #}, {# 1, 'C' #}, {# 2, 'A' #}, {# 2, 'B' #}, {# 2, 'C' #}, {# 3, 'A' #}, {# 3, 'B' #}, {# 3, 'C' #}]
crossByThe function crossBy(lst1,lst2,fun) returns a list in
which each element is a result of applying fun(x,y) to all
combinations of elements of the lists lst1 and
lst2. The order of the elements is the same as with
cross.
crossBy( ['a','b','c'], ['A','B','C'], operator+ )['aA', 'aB', 'aC', 'bA', 'bB', 'bC', 'cA', 'cB', 'cC']
crossByIdxThe function crossByIdx(lst1,lst2,fun) is similar to
crossBy, but the function will get not only the values from
lst1 and lst2 but also their positions in the
lists (i.e. indices).
crossByIdx( ['a','b','c'], ['A','B','C'],
\ix,x,iy,y: '[' + ix$ + ']' + x + '-[' + iy$ + ']' + y
)['[0]a-[0]A', '[0]a-[1]B', '[0]a-[2]C', '[1]b-[0]A', '[1]b-[1]B', '[1]b-[2]C', '[2]c-[0]A', '[2]c-[1]B', '[2]c-[2]C']
foldr, foldl and foldFunction / Type and Description
foldr
(Sequence['3]['2] * ('2 * '1 -> '1) * '1 -> '1)
Returns the right associative fold of sequence elements:
foldr([a,b,c],fn,zero) = fn(a,fn(b,fn(c,zero)))
foldl
(Sequence['3]['2] * ('1 * '2 -> '1) * '1 -> '1)
Returns the left associative fold of sequence elements:
foldl([a,b,c],fn,zero) = fn(fn(fn(zero,a),b),c)
fold
(Sequence['2]['1] * ('1 * '1 -> '1) * '1 -> '1)
Returns the sequence fold by an associative function:
fold == foldl == foldr
foldrLike the map, the function
foldr( lst, fun, zero ) is one of the most important higher
order functions. It combines the elements of the list lst,
using zero as a start value and applying
fun(x,res) to combine the element x with the
result res of the already completed combination.
This function is equivalent to our combineElements
function from the previous examples. So let us use it in a similar
example:
[1,2,3,4,5].foldr( operator+, 0 )15
To better understand what foldr evaluates, consider the
following example:
['1','2','3','4','5'].foldr( \x,res: '('+x+','+res+')', '' )(1,(2,(3,(4,(5,)))))
As we can see, foldr combines elements by combining the
head of the list with the already combined list tail. It really starts
combining from the end of the list, or from the right side,
which is why it has an r at the end of the name.
foldlIf the combining function fun is an associative function
(i.e. fun(fun(a,b),c) = fun(a,fun(b,c))), then
the order of the combination is not important. But for non-associative
functions, as in the previous example, the order is important. For this
reason, there is foldl function.
The function foldl combines the list elements from the
beginning. It first combines zero and the list head and
then the result with the next list element and so on. Look at the
following example and compare the result with the previous example where
foldr was used with the same arguments:
['1','2','3','4','5'].foldl( \x,res: '('+x+','+res+')', '' )(((((,1),2),3),4),5)
If the result of the combining operation has the same type as the
list elements, then the combining function has both the arguments and
the result of the same type. As we have already emphasized, if the
combining function is associative, then foldr and
foldl return the same result. Here we can verify this for
the addition:
{#
['1','2','3','4','5'].foldr( operator+, '' ),
['1','2','3','4','5'].foldl( operator+, '' )
#}{# '12345', '12345' #}
On the other hand, if the function is not associative but has the same argument types, then the result will not be the same:
{#
lst.foldr( fun, '' ),
lst.foldl( fun, '' )
#}
where {
lst = ['1','2','3','4','5'];
fun(x,y) = '(' + x + ',' + y +')';
}{# '(1,(2,(3,(4,(5,)))))', '(((((,1),2),3),4),5)' #}
As we can see, foldl combines elements by combining the
given initial value with the head of the list, assuming that the result
will be passed on to the tail to do the same processing there. So, it
really starts combining from the beginning of the list, or from the
left side, which is why it has an l at the end of the
name.
There are many combination functions that cannot be used with both
foldr and foldl. If the result of the
combination has a different type than the list elements, then the
combining functions have different types of arguments. Such functions
cannot be used with both foldr and foldl.
For example, let us write the function append2 that
appends two lists (the equivalent of the library function
append and the operator ++). We can do this by
combining the elements of the first list with the cons-operator, and
specifying the second list as zero. Since the cons-operator
is not associative (it also has different argument types), we can only
use foldr:
[1,2,3].foldr( operator:, [4,5,6])[1, 2, 3, 4, 5, 6]
We can append many lists together in a similar way:
[[1,2],[3,4],[5,6]].foldr( append, [])[1, 2, 3, 4, 5, 6]
To use the cons-operator with foldl we need to reverse
the arguments. However, the result of the application will be a reversed
list:
[1,2,3,4,5,6].foldl( \x,y: y:x, [] )[6, 5, 4, 3, 2, 1]
The following example shows how to use foldr to compute
a list of distinct elements of a sorted list. The combining lambda
function assumes that the second argument is the result of the tail
processing:
(1..100)
.map( operator%(_,10) )
.sort()
.distinct()
where {
distinct( lst ) =
lst.foldr(
\x,dist:
if dist.empty() or x != dist.hd()
then x : dist
else dist
, []
);
}[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
foldIf (1) the combining function fun is an associative
function (i.e. fun(fun(a,b),c) =
fun(a,fun(b,c))) with arguments of the same type and (2)
the given initial value is really a zero (neutral value) for the given
combining function fun, then foldl and
foldr return the same results.
The function fold is intended for such cases when it is
not important in which direction the sequence elements are combined.
This function can also be parallelized, which we will discuss later.
Function / Type and Description
select
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition
selectN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2]['1])
Extracts at most N elements that meet the given condition.
selectFrom
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements, from the first one that satisfies the condition, until the end.
selectWhile
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements from the first one until the one not satisfying the condition.
selectUntil
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements from the first one until the one satisfying the condition.
selectByIdx
(Sequence['2]['1] * Sequence['2][Int] -> Sequence['2]['1])
Extracts the sequence elements with given indexes.
selectTrue
(Sequence['2]['1] * Sequence['2][Bool] -> Sequence['2]['1])
Extracts the sequence elements for which the corresponding flag is true.
selectDistinct
(Sequence['2]['1] -> Sequence['2]['1])
Extracts the locally distinct sequence elements.
select_par
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that fulfill the given condition.
select_seq
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition.
selectMap
(Sequence['3]['1] * ('1 -> Bool) * ('1 -> '2) -> Sequence['3]['2])
Maps a function to sequence elements that meet the condition.
selectMap(seq,cond,fn) == map(select(seq,cond),fn)
mapSelect
(Sequence['3]['1] * ('1 -> '2) * ('2 -> Bool) -> Sequence['3]['2])
Maps a function to sequence elements and filter the results.
mapSelect(seq,fn,cond) == select(map(seq,fn),cond)
The following functions are equivalent, but deprecated: ::: {.libcaption} Function / Type and Description :::
filter
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition
filter_par
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that fulfill the given condition.
[Deprecated. Use select_par.]
filter_seq
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition.
[Deprecated. Use select_seq.]
filterN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2]['1])
Extracts at most N elements that meet the given condition.
[Deprecated. Use selectN.]
filterMap
(Sequence['3]['1] * ('1 -> Bool) * ('1 -> '2) -> Sequence['3]['2])
Maps a function to sequence elements that meet the condition.
filterMap(seq,cond,fn) == map(filter(seq,cond),fn)
[Deprecated. Use selectMap.]
mapFilter
(Sequence['3]['1] * ('1 -> '2) * ('2 -> Bool) -> Sequence['3]['2])
Maps a function to sequence elements and filter the results.
mapFilter(seq,fn,cond) == filter(map(seq,fn),cond)
[Deprecated. Use mapSelect.]
select,
select_seq and select_parThe function select(lst,cond) selects only the
lst elements that fulfill the given condition
cond(x). The order of the elements is retained.
{#
[1,2,3,4,5].select( operator>(_,1)), // numbers > 1
[1,2,3,4,5].select( \x : x%2 > 0 ), // odd numbers
[1,2,3,4,5].select( \x : x%2 = 0 ) // even numbers
#}{# [2, 3, 4, 5], [1, 3, 5], [2, 4] #}
The functions select_seq and select_par are
equivalent to select, but force the sequential or parallel
evaluation. The select function is automatically determined
to evaluate in parallel or in sequential mode.
The equivalent functions filter, filter_seq
and filter_par are deprecated.
selectNThe function selectN(lst,cond,n) selects at most
n elements of the lst that fulfill the given
condition cond(x). The order of the elements is
retained.
{#
1..7 =>selectN( operator>(_,1), 2 ), // numbers > 1
1..7 =>selectN( \x : x%2 > 0, 2 ), // 2 odd numbers
1..7 =>selectN( \x : x%2 = 0, 2 ) // 2 even numbers
#}{# [2, 3], [1, 3], [2, 4] #}
The equivalent function filterN is deprecated.
selectFromThe function selectFrom(lst,cond) selects the
lst elements from the first one that fulfills the given
condition cond(x), to the end of the list. The order of the
elements is retained.
{#
[1,2,3,4,5].selectFrom( operator=(_,2)), // elements from 2 to the end
[1,2,3,4,5].selectFrom( \x: x%4 = 2 ) // elements from 2 to the end
#}{# [2, 3, 4, 5], [2, 3, 4, 5] #}
selectWhileThe function selectWhile(lst,cond) selects the
lst elements from the beginning, while they all fulfill the
given condition cond(x). The order of the elements is
retained.
{#
[1,2,3,4,5].selectWhile( operator!=(_,3)), // elements before 3
[1,2,3,4,5].selectWhile( \x: x%4 != 0 ), // elements before 4
#}{# [1, 2], [1, 2, 3] #}
selectUntilThe function selectUntil(lst,cond) selects the
lst elements from the beginning, until the first one that
fulfills the given condition cond(x). The order of the
elements is retained.
{#
[1,2,3,4,5].selectUntil( operator=(_,3)), // elements before 3
[1,2,3,4,5].selectUntil( \x: x%4 = 0 ), // elements before 4
#}{# [1, 2], [1, 2, 3] #}
selectByIdxThe function selectByIdx(lst,idxLst) selects the
lst elements with indices in the idxLst. The
order of the elements is as specified in the idxList. If an
index is out of the lst bounds, it is used a
idx % size(lst), the same way as index operator works:
lst[idx] == lst[idx % size(lst)].
{#
[1,2,3,4,5].selectByIdx( [0,3,2] ),
[1,2,3,4,5].selectByIdx( [10,13,12] ),
[1,2,3,4,5].selectByIdx( [] ),
[].selectByIdx( [10,13,12] )
#}{# [1, 4, 3], [1, 4, 3], [], [] #}
selectTrueThe function selectTrue(lst,boolLst) selects the
lst elements where the corresponding boolLst
element is true. The order of the elements is retained. If
the boolLst is longer than lst, only the first
size(lst) elements are used.
{#
[1,2,3,4,5].selectTrue( [true,false,true] ),
[1,2,3].selectTrue( [false,true,false,true,false,true] ),
[1,2,3,4,5].selectTrue( [] ),
[].selectTrue( [true] )
#}{# [1, 3], [2], [], [] #}
selectDistinctThe function selectDistinct(lst) selects the locally
distinct lst elements, i.e. the elements that are not the
same as their predecessor. The order of the elements is retained.
{#
[1,2,2,3,3,3,1,1,1,3,3,3,1,1,1].selectDistinct(),
[].selectDistinct()
#}{# [1, 2, 3, 1, 3, 1], [] #}
To select globally distinct elements, the list has to be sorted
first: lst.sort().selectDistinct().
selectMapThe function select is often used to select some list
elements before they are processed by map. The
selectMap function performs both selecting and processing
in one step.
The function selectMap(lst,selFn,mapFn) is equivalent to
the appropriate combination lst.select(selFn).map(mapFn),
but is more efficient. The following example computes a list of squares
from odd list elements:
{#
list.selectMap( odd, square ),
list.select(odd).map(square)
#}
where {
list = 1..20;
odd( n ) = n % 2 != 0;
square( n ) = n * n;
}{# [1, 9, 25, 49, 81, 121, 169, 225, 289, 361], [1, 9, 25, 49, 81, 121, 169, 225, 289, 361] #}
mapSelectThe function mapSelect(lst,mapFn,selFn) does the
opposite to the selectMap - it first maps the values using
mapFn and that selects the results that fulfill the given
selFn.
The following example computes a list the squares of the list elements, and selects only the results between 50 and 100:
{#
list.mapSelect( square, \x:x>50 and x<100 ),
list.map(square).select( \x:x>50 and x<100 )
#}
where {
list = 1..20;
square( n ) = n * n;
}{# [64, 81], [64, 81] #}
The functions selectMap and mapSelect do
not make the program code more readable, but they are more efficient
that map/select pair, because they do not
create temporary intermediate lists.
filter,
filter_seq, filter_par, filterN,
filterMap and mapFilterThe functions filter, filter_seq,
filter_par, filterN, filterMap
and mapFilter are deprecated synonyms for
select, select_seq, select_par,
selectN, selectMap and
mapSelect.
Function / Type and Description
findFirst
(Sequence['2]['1] * ('1 -> Bool) -> Int)
Finds the index of the first element that meets the condition.
find
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2][Int])
Finds the indexes of the elements that meet the given condition.
findN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2][Int])
Finds at most N indices of elements that meet the given condition.
findEqFirst
(Sequence['2]['1] * '1 -> Int)
Finds the index of the first element equal to the given value.
findEq
(Sequence['2]['1] * '1 -> Sequence['2][Int])
Finds the indexes of the elements equal to the given value.
findEqN
(Sequence['2]['1] * '1 * Int -> Sequence['2][Int])
Finds at most N indices of the elements equal to the given value.
findFirstThe function findFirst(lst,cond) returns the zero-based
index of the first lst element that fulfills the given
condition cond. If such element is not found, returns
-1.
{#
[1,2,3,1,2,3].findFirst( operator>(_,1)), // number > 1
[1,2,3,1,2,3].findFirst(\x: x%2 > 0 ), // odd number
[1,2,3,1,2,3].findFirst(\x: x = 3 ), // number 3
[1,2,3,1,2,3].findFirst(\x: x = 5 ) // number 5
#}{# 1, 0, 2, -1 #}
findThe function find(lst,cond) returns the list of
zero-based indices of the elements of lst that fulfill the
given condition cond(x). The indices are selected in
ascending order.
{#
[1,2,3,4,5].find( operator>(_,1)), // numbers > 1
[1,2,3,4,5].find(\x: x%2 > 0 ), // odd numbers
[1,2,3,1,2,3].find(\x: x = 3 ), // numbers 3
[1,2,3,1,2,3].find(\x: x = 5 ) // numbers 5
#}{# [1, 2, 3, 4], [0, 2, 4], [2, 5], [] #}
findNThe function findN(lst,cond,n) returns the list of
zero-based indices of the elements of lst that fulfill the
given condition cond(x), but it selects at most the first
n such indices. The indices are selected in ascending
order.
{#
[1,2,3,4,5].findN( operator>(_,1), 2), // numbers > 1
[1,2,3,4,5].findN(\x: x%2 > 0, 2 ), // odd numbers
[1,2,3,1,2,3].findN(\x: x = 3, 1 ), // numbers 3
[1,2,3,1,2,3].findN(\x: x = 5, 1 ) // numbers 5
#}{# [1, 2], [0, 2], [2], [] #}
findEqFirstThe function findEqFirst(lst,x) returns the zero-based
index of the first lst element equal to x. If
the element is not found, returns -1.
{#
[1,2,3,1,2,3].findEqFirst( 1 ),
[1,2,3,1,2,3].findEqFirst( 2 ),
[1,2,3,1,2,3].findEqFirst( 3 ),
[1,2,3,1,2,3].findEqFirst( 4 )
#}{# 0, 1, 2, -1 #}
findEqThe function findEq(lst,x) returns the list of
zero-based indices of the elements of lst equal to the
given value x. The indices are selected in ascending
order.
{#
[1,2,3,1,2,3].findEq( 1 ),
[1,2,3,1,2,3].findEq( 2 ),
[1,2,3,1,2,3].findEq( 3 ),
[1,2,3,1,2,3].findEq( 4 )
#}{# [0, 3], [1, 4], [2, 5], [] #}
findEqNThe function findEqN(lst,x,n) returns the list of
zero-based indices of the elements of lst equal to the
given value x, but it selects at most the first
n such indices. The indices are selected in ascending
order.
{#
[1,2,3,1,2,3,1,2,3].findEqN( 1, 2 ),
[1,2,3,1,2,3,1,2,3].findEqN( 2, 2 ),
[1,2,3,1,2,3,1,2,3].findEqN( 3, 1 ),
[1,2,3,1,2,3,1,2,3].findEqN( 4, 2 )
#}{# [0, 3], [1, 4], [2], [] #}
forall
and existsFunction / Type and Description
forall
(Sequence['2]['1] * ('1 -> Bool) -> Bool)
Checks whether the given condition applies to all sequence elements:
forall(list,cond).
exists
(Sequence['2]['1] * ('1 -> Bool) -> Bool)
Checks whether any sequence element fulfills the condition:
exists(sequence,cond)
The function forall(lst,cond) checks whether all list
elements fulfill the given condition cond(x). It returns
the same result as the combination of the mapping cond and
then the folding the result with and:
forall( lst , cond )
==
lst.map(cond).foldl( operator&&, true )The function exists(lst,cond) checks whether a list
element fulfills the given condition cond(x). It is
equivalent to mapping the cond and then folding the result
with or:
exists( lst , cond )
==
lst.map(cond).foldr( operator||, false ){#
[1,2,3,4,5].forall( operator>(_,1) ),
[1,2,3,4,5].forall( operator>=(_,1) ),
[1,2,3,4,5].exists( operator<(_,1) ),
[1,2,3,4,5].exists( operator<=(_,1) )
#}{# false, true, false, true #}
The functions forall and exists are defined
in a lazy way. They process the list elements from the head and can stop
processing before the end of the list if the result is already known.
The forall function stops after the first negative result
and the exists function stops after the first positive
result.
count
and countRepFunction / Type and Description
count
(Sequence['2]['1] * ('1 -> Bool) -> Int)
Counts the sequence elements that fulfill the condition:
count(seq,cond)
countRep
(Sequence['2]['1] -> Map['1][Int])
Creates a catalog from the sequence. Keys are the distinct elements, and values are the number of occurrences.
The function count(lst,cond) counts how many list
elements fulfill the given condition cond(x).
{#
[1,2,3,4,5].count( operator>(_,2) ),
[1,2,3,4,5].count( operator>=(_,2) )
#}{# 3, 4 #}
The function countRep(lst) counts how often each
individual element in the list is repeated. It returns a catalog in
which the keys are all unique list elements and the values are the
number of occurrences.
Please see Map Type for details.
['a','b','a','a','b','c'].countRep()[<'a',3>, <'b',2>, <'c',1>]
['a','b','a','a','b','c'].countRep()['b']2
sort and
sortByFunction / Type and Description
sort
(Sequence['2][Value['1]] -> Sequence['2][Value['1]])
Sorts value sequence.
sortBy
(Sequence['2]['1] * ('1 * '1 -> Bool) -> Sequence['2]['1])
Sorts the sequence using given comparator.
The function sort(lst) returns a list sorted in
ascending order. It works for sequences with elements of primitive
types.
The function sortBy(lst,lessThan) returns a list sorted
in ascending order using the given comparison function
lessThan(x,y). It works for sequences with any element
types if a corresponding comparison function is specified.
{#
[2,1,4,3,5].sort(),
[2,1,4,3,5].sortBy( operator< ),
[2,1,4,3,5].sortBy( operator> )
#}{# [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [5, 4, 3, 2, 1] #}
There are two specific sorting functions for strings, to explicitly
specify which collation is used: the sortAsii function is a
synonym for sort and compares strings as regular sequences
of single-byte characters; the sortUtf8 function uses UTF-8
multi-byte collation as defined by the operating system locale.
It is a known issue that Windows and Linux use different collates for UTF-u locale. For example. symbols ‘$’ and ‘(’ do not compare in the same way.
in and
containsFunction / Type and Description
in
('1 * Sequence['2]['1] -> Bool)
Checks whether the element is in the sequence:
el.in(seq)
contains
(Sequence['2]['1] * '1 -> Bool)
Checks whether the element is contained in the sequence:
seq.contains(el)
The function in(x,lst) checks whether the list
lst contains x. It is normally used in the
form x.in(lst). It works for sequences of any type.
{#
1 .in([1,2,3]),
11 .in([1,2,3]),
{a:5}.in([ {a:4}, {a:5} ]),
{a:6}.in([ {a:4}, {a:5} ])
#}{# true, false, true, false #}
The function contains does the same, but with the
operands in reverse order: contains(lst,x) is the same as
in(x,lst).
{#
[1,2,3].contains( 1 ),
[1,2,3].contains( 11 ),
[ {a:4}, {a:5} ].contains( {a:5} ),
[ {a:4}, {a:5} ].contains( {a:6} )
#}{# true, false, true, false #}
Wafl is an eager language, but as in the most languages, there are some lazy elements to avoid unnecessary computation. One such lazy element is retrieving the results of a database query. The query result is a lazy list - only as many rows are retrieved from the database server as are required for the list processing.
However, sometimes we know that we need to process all the rows, but
this may take some time and it may make sense to end the communication
with the database server as soon as possible. In such cases, we can
request the eager evaluation of the query result list with the
force function.
Function / Type and Description
forced
(List['1] -> List['1])
Returns the list, but forces any delayed evaluation.
The function force(lst) returns the same list, but
forces its eager evaluation.
The array is a sequence type, like a list, but with a focus on the efficiency of indexing and slicing operations.
The general array type is written as Array['1], where
'1 denotes a type variable (a pure polymorphic
type). Each specific array in a program must have a specific type,
where this free type is fully defined.
The syntax of array literals is similar to that of list literals. The only difference is that square brackets with hash symbols are used instead of square brackets.
{#
[# #],
[#1,2,3#], // Array[Int]
[#'a','b','c','d'#], // Array[String]
[#[#1,2#],[#3,4,5#],[#6,7,8#]#] // Array[Array[Int]]
#}{# [# #], [# 1, 2, 3 #], [# 'a', 'b', 'c', 'd' #], [# [# 1, 2 #], [# 3, 4, 5 #], [# 6, 7, 8 #] #] #}
It is allowed to specify a comma separator after the last element of an array. It has no function there, but it makes it easier to edit, copy, and comment/uncomment array elements.
Many of the list functions also work with arrays:
Function / Type and Description
A == B
('1 * '1 -> Bool)
Equal-to operator.
A != B
('1 * '1 -> Bool)
Not-equal-to operator.
empty
(Indexable['1]['2]['3] -> Bool)
Checks whether the collection is empty.
size
(Indexable['1]['2]['3] -> Int)
Returns the size of the collection.
length
(Indexable['1]['2]['3] -> Int)
Returns the size of the collection.
longerThan
(SequenceStr['1]['2] * Int -> Bool)
Checks whether the sequence is longer than the given integer.
sub
(SequenceStr['2]['1] * Int * Int -> SequenceStr['2]['1])
Extracts the subsequence from given 0-based position and given
length: sub(seq,pos,len)
A[ : B ]
(SequenceStr['2]['1] * Int -> SequenceStr['2]['1])
Extracts a prefix of the sequence with N elements:
seq[:N]
A[ B : C ]
(SequenceStr['2]['1] * Int * Int -> SequenceStr['2]['1])
Extracts a sequence segment from Nth to (M-1)th element:
seq[N:M]
A[ B : ]
(SequenceStr['2]['1] * Int -> SequenceStr['2]['1])
Extracts a suffix of the sequence with N elements:
seq[N:]
A[ B ]
(Indexable['1]['2]['3] * '2 -> '3)
Extracts an element from the indexable collection:
col[idx]
A ++ B
(SequenceStr['1]['2] * SequenceStr['1]['2] -> SequenceStr['1]['2])
Appends the the second sequence to the first sequence.
map
(Sequence['2]['1] * ('1 -> '3) -> Sequence['2]['3])
Maps the function to sequence elements: map(seq,fun)
mapIdx
(Sequence['1]['2] * (Int * '2 -> '3) -> Sequence['1]['3])
Maps the function to sequence elements:
mapIdx(seq,fun)
zip
(Sequence['4]['1] * Sequence['4]['2] -> Sequence['4][Tuple['1, '2]])
Zips two sequences to a sequence of pairs.
zipBy
(Sequence['4]['1] * Sequence['4]['2] * ('1 * '2 -> '3)
-> Sequence['4]['3]) Zips two sequences by given
function: zipBy(seq1, seq2 ,fun)
zipByIdx
(Sequence['4]['1] * Sequence['4]['2] * (Int * '1 * '2 -> '3)
-> Sequence['4]['3]) Zips two sequences and indexes
by given function: zipByIdx(seq1, seq2 ,fun)
fold
(Sequence['2]['1] * ('1 * '1 -> '1) * '1 -> '1)
Returns the sequence fold by an associative function:
fold == foldl == foldr
foldr
(Sequence['3]['2] * ('2 * '1 -> '1) * '1 -> '1)
Returns the right associative fold of sequence elements:
foldr([a,b,c],fn,zero) = fn(a,fn(b,fn(c,zero)))
foldl
(Sequence['3]['2] * ('1 * '2 -> '1) * '1 -> '1)
Returns the left associative fold of sequence elements:
foldl([a,b,c],fn,zero) = fn(fn(fn(zero,a),b),c)
select
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition
selectN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2]['1])
Extracts at most N elements that meet the given condition.
selectFrom
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements, from the first one that satisfies the condition, until the end.
selectWhile
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements from the first one until the one not satisfying the condition.
selectUntil
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements from the first one until the one satisfying the condition.
selectByIdx
(Sequence['2]['1] * Sequence['2][Int] -> Sequence['2]['1])
Extracts the sequence elements with given indexes.
selectTrue
(Sequence['2]['1] * Sequence['2][Bool] -> Sequence['2]['1])
Extracts the sequence elements for which the corresponding flag is true.
selectDistinct
(Sequence['2]['1] -> Sequence['2]['1])
Extracts the locally distinct sequence elements.
select_par
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that fulfill the given condition.
select_seq
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition.
selectMap
(Sequence['3]['1] * ('1 -> Bool) * ('1 -> '2) -> Sequence['3]['2])
Maps a function to sequence elements that meet the condition.
selectMap(seq,cond,fn) == map(select(seq,cond),fn)
mapSelect
(Sequence['3]['1] * ('1 -> '2) * ('2 -> Bool) -> Sequence['3]['2])
Maps a function to sequence elements and filter the results.
mapSelect(seq,fn,cond) == select(map(seq,fn),cond)
find
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2][Int])
Finds the indexes of the elements that meet the given condition.
findN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2][Int])
Finds at most N indices of elements that meet the given condition.
forall
(Sequence['2]['1] * ('1 -> Bool) -> Bool)
Checks whether the given condition applies to all sequence elements:
forall(list,cond).
exists
(Sequence['2]['1] * ('1 -> Bool) -> Bool)
Checks whether any sequence element fulfills the condition:
exists(sequence,cond)
in
('1 * Sequence['2]['1] -> Bool)
Checks whether the element is in the sequence:
el.in(seq)
contains
(Sequence['2]['1] * '1 -> Bool)
Checks whether the element is contained in the sequence:
seq.contains(el)
count
(Sequence['2]['1] * ('1 -> Bool) -> Int)
Counts the sequence elements that fulfill the condition:
count(seq,cond)
sort
(Sequence['2][Value['1]] -> Sequence['2][Value['1]])
Sorts value sequence.
sortBy
(Sequence['2]['1] * ('1 * '1 -> Bool) -> Sequence['2]['1])
Sorts the sequence using given comparator.
append
(SequenceStr['1]['2] * SequenceStr['1]['2] -> SequenceStr['1]['2])
Appends the second sequence to the first sequence.
appendAll
(Sequence['1][SequenceStr['2]['3]] -> SequenceStr['2]['3])
Appends all sequences in the given sequence.
groupBy
(Sequence['3]['1] * ('1 -> '2) -> Map['2][List['1]])
Creates a catalog from the sequence. Each catalog element is a list of all elements mapped to the key value by the given function.
countRep
(Sequence['2]['1] -> Map['1][Int])
Creates a catalog from the sequence. Keys are the distinct elements, and values are the number of occurrences.
These functions have already been introduced for the list type and will not be discussed here again. Their application to arrays is identical to that for lists. The only difference is that the arguments and the results are arrays instead of lists.
newArray and newArrayFnFunction / Type and Description
newArray
(Int * '1 -> Array['1])
Generates an array with given number of elements of given value:
newArray(5,'a') == [# 'a', 'a', 'a', 'a', 'a' #]
newArrayFn
(Int * (Int -> '1) -> Array['1])
Generates an array with given number of elements and computes each of
the elements using given function on its index:
newArrayFn(5,\x:x*x) == [# 0, 1, 4, 9, 16 #]
The function newArray(n,x) returns an array with
n elements, where all elements are set to
x.
The function newArray(n,fn) returns an array with
n elements, whereby the element at position
idx is initialized as fn(idx).
{#
newArray( 5, 1 ),
newArray( 5, 'a' ),
newArrayFn( 5, \x:x ),
newArrayFn( 5, \x:x*x ),
newArrayFn( 5, \x: random(5) )
#}{# [# 1, 1, 1, 1, 1 #], [# 'a', 'a', 'a', 'a', 'a' #], [# 0, 1, 2, 3, 4 #], [# 0, 1, 4, 9, 16 #], [# 1, 1, 4, 1, 2 #] #}
Arrays and lists are similar, but not the same types. They are both sequences, but have a different internal structure. Arrays are assumed to have a fixed content, which is usually used for indexing and slicing, while lists are often used by decomposing into head and tail. The usual sequence operations work the same for both.
Furthermore, the internal implementation of the list type can be the same as that of the array type. Depending on the context and the creation method used, the interpreter decides whether a list is structured as usual or as an array.
For this reason, there are functions for converting a list into an
array and vice versa. To force the internal array-like organization of a
list, convert it into an array and then back into a list:
lst.asArray().asList().
Function / Type and Description
asArray
(Sequence['1]['2] -> Array['2])
Converts a sequence to an array
asList
(Sequence['1]['2] -> List['2])
Converts a sequence to a list
{#
[].asArray(),
[##].asList(),
[1,2,3].asArray(),
[1,2,3].asArray().asList(),
['1','2','3'].asArray(),
['1','2','3'].asArray().asList(),
[#1,2,3#] == [#1,2,3#],
[#1,2,3#] == [#1,2,3,4,5#],
[#1,2,3#] != [#1,2,3#],
[#1,2,3#] != [#1,2,3,4,5#]
#}{# [# #], [], [# 1, 2, 3 #], [1, 2, 3], [# '1', '2', '3' #], ['1', '2', '3'], true, false, false, true #}
The map is an indexable type. It has a catalog structure in which the elements are accessed via keys. Both the keys and the values can be of any valid type.
createMapThere are no map literals in Wafl. The maps are normally created with
the createMap function.
Function / Type and Description
createMap
(Array[Value['1]] * Array['2] -> Map[Value['1]]['2])
Creates a catalog from given arrays.
The function createMap takes two arrays of the same size
and creates a map with the keys from the first array and the
corresponding values from the second.
The elements are accessed by indexing.
In the following example, a map is created with string keys and list values. The map is indexed by some keys:
{#
aMap['a'],
aMap
#}
where {
aMap = createMap(
[# 'a', 'b', 'c' #],
[#
[1,2,3],
[4,5],
[6]
#]
);
}{# [1, 2, 3], [<'a',[1, 2, 3]>, <'b',[4, 5]>, <'c',[6]>] #}
Map is not a safe type. The behavior of the indexing operator is not defined if a non-existent index is used.
groupByIf it is necessary to categorize list elements according to certain
criteria, it can be useful to create a map that contains different
values of the categorization criteria as keys and the lists of
categorized elements as values. This is what the function
groupBy computes.
Function / Type and Description
groupBy
(Sequence['3]['1] * ('1 -> '2) -> Map['2][List['1]])
Creates a catalog from the sequence. Each catalog element is a list of all elements mapped to the key value by the given function.
For example, we will categorize a list of integers by their reminder when dividing by 4:
{# aMap, aMap[1] #}
where {
aMap = (1..20).groupBy(\x:x%4);
}{# [<1,[1, 5, 9, 13, 17]>, <2,[2, 6, 10, 14, 18]>, <3,[3, 7, 11, 15, 19]>, <0,[4, 8, 12, 16, 20]>], [1, 5, 9, 13, 17] #}
The usual way to use the maps is to index them. However, if it is not clear which exact key values are allowed, some other usage methods may be useful.
keys and
valuesFunction / Type and Description
keys
(Map['2]['1] -> Array['2])
Returns map keys.
values
(Map['2]['1] -> Array['1])
Returns map values.
key
(Map['2]['1] * Int -> '2)
Returns Nth key (from 0).
value
(Map['2]['1] * Int -> '1)
Returns Nth value.
The functions keys and values return the
arrays that were used to build the map.
The functions key(n) and value(n) return
the nth key (or value) from the corresponding arrays. They
are equivalent to indexing the arrays, such as keys()[n]
and values()[n].
{#
aMap.keys(),
aMap.key(0),
aMap.values(),
aMap.value(0)
#}
where {
aMap = createMap([# 'a', 'b', 'c' #], [# 11, 22, 33 #]);
}{# [# 'a', 'b', 'c' #], 'a', [# 11, 22, 33 #], 11 #}
hasKey, hasValue, findKey and
findValueFunction / Type and Description
hasKey
(Map['2]['1] * '2 -> Bool)
Checks whether there is a given key in the map.
hasValue
(Map['2]['1] * '1 -> Bool)
Checks whether there is a given value in the map.
findKey
(Map['2]['1] * '2 -> Int)
Finds a position of the given key, or -1 if not found.
findValue
(Map['2]['1] * '1 -> Int)
Finds a position of the given value, or -1 if not found.
The functions hasKey and hasValue check
whether there is a map key (or value) with the given value.
The functions findKey and findValue search
for the key (or value) in the corresponding array and return the index
of the key (or value) found, or -1 if it was not found.
So if a key x exists in a map m, then the
following must be true:
m.keys()[ m.findKey(x) ] == x
m.key( m.findKey(x) ) == x
and the same applies to the values.
{#
aMap['a'],
aMap.hasKey('a'),
aMap.hasKey('A'),
aMap['c'],
aMap.findKey('c'),
aMap.findKey('C')
#}
where {
aMap = createMap([# 'a', 'b', 'c' #], [# 11, 22, 33 #]);
}{# 11, true, false, 33, 2, -1 #}
Tuple is a structured data type. It consists of a series of unnamed elements that are referenced by a position. Tuple elements can have different types.
Tuples are written with curly brackets and hashes:
{# ... #}, and tuple elements are separated by commas. The
elements can be listed as literals or as expressions.
Tuples must not be empty.
It is allowed to specify an element separator after the last element.
Although the syntax of a hanging separator may seem too loose, it is very useful in practice. Tuple literals are often written with one element in a line (see the following example). If a hanging separator were not allowed in such cases, commenting the last element would require deleting or commenting the previous separator.
In the following example there is a tuple with three tuple elements of different types.
{#
{# 1 #},
{# 1 + 2,'a'+'b' #},
{# 1,{# 'list',[1,2,3] #}#},
// 'last' - this element is declared as a comment
// and the previous separator `,` is hangs, but that's OK
#}{# {# 1 #}, {# 3, 'ab' #}, {# 1, {# 'list', [1, 2, 3] #} #} #}
The type of this tuple is:
Tuple[
Tuple[Integer],
Tuple[Integer, String],
Tuple[Integer,Tuple[String,List[Integer]]]
]
Tuple elements are accessed via tuple selectors. As tuple elements are not named, their position is used to select them. The positions of the tuple elements are 1-based.
Tuple selectors use the dot-syntax
<tuple>.<n>, where a positive integer indicates
the position of the element to be read.
{# 1, 2, 3, 4, 5 #}.f()
where{
f(t) = {#
t.1,
t.1 + t.2,
t.1 + t.2 + t.3,
t.1 + t.2 + t.3 + t.4,
t.1 + t.2 + t.3 + t.4 + t.5
#};
}{# 1, 3, 6, 10, 15 #}
The selector indices and element types are checked in the type-checking phase so that it is not possible to use an invalid selector.
To support the syntax similar to records, the symbol ‘$’
can be used instead of the dot. The two syntaxes are equivalent -
x$1 is the same as x.1.
Tuple updaters are operators that return a copy of a tuple in which one of the tuple elements has been replaced by the given value. They are called updaters, but they do not modify the given tuple - instead they construct a new tuple with the copied and changed values.
Tuple updaters have the following basic syntax:
<tuple>^<n>(<value>), where a positive
integer <n> specifies the position of the element to
be replaced and the <value> specifies its new
value.
{#
{# 1, 2, 3 #}^1(11),
{# 1, 2, 3 #}^2(22),
{# 1, 2, 3 #}^3(33)
#}{# {# 11, 2, 3 #}, {# 1, 22, 3 #}, {# 1, 2, 33 #} #}
As with the selectors, the indexes and types of the updaters are checked in the type-checking phase, so that it is not possible to use an invalid selector.
The tuple data type is written as follows:
Tuple[<el1-type>, <el2-type>, ..., <eln-type>]
If T1 is a tuple type with N elements, and
T2 is another tuple type with K elements,
where 0 < K < N, and for
each i (0 < i <=
K) the type of the ith element of
T1 is the same as the type of the ith element
of T2, then T1 is a subtype of
T2.
If a selector works with a tuple of type T2, then it
also works for T1 (this is obvious: the selector has the
index n <= K, so it is also n
<= N and works with T1). The same rule also
applies to updaters.
If a function is defined in a such way that it works with an argument of a tuple type, then it can also be applied to subtypes of this type.
In the following example, the function f sums the first
two elements of a tuple. The function can be applied to all tuples with
at least two elements, whereby the first two elements are naturally of
the same value type:
{#
{# 1, 2 #}.f(),
{# 1, 2, 3 #}.f(),
{# 1, 2, 'a string' #}.f(),
{# 1.2, 2.4, 'a string' #}.f(),
{# 'a', 'b', 1.2, 2.4, 'a string' #}.f()
#}
where {
f(t) = t.1 + t.2;
}{# 3, 3, 3, 3.5999999999999996, 'ab' #}
The types of the .2 selector and the ^2
updater are for example:
(TupleX['1,'2]['3] -> '2)
(TupleX['1,'2]['3] * 2 -> Tuple['1,'2]['3])
where TupleX stands for extendable tuple and
'3 denotes an optional tuple extension type. This is
necessary for the subtypes to work properly.
On the other hand, each tuple has an exact tuple type. For example,
the result tuple in the last example has the type:
Tuple[ Int, Int, Int, Float, String ].
Record is a structured data type. It consists of a series of named values. Record elements can have different types.
Literal records are written in curly brackets { ... }.
The elements are separated by commas or semicolons. Each element has a
name and a value. Names and values are separated by colons
‘:’ or the equals sign ‘=’.
The syntax is flexible in order to be compatible with different languages, but it must be strictly adhered to - it is not permitted to combine different separators in one record.
As with tuples, it is allowed to specify an element separator after the last element.
Although the syntax of a hanging separator may seem too loose, it is very useful in practice. Record literals are often written with one element in a line (see the following example). If a hanging separator were not allowed in such cases, commenting the last element would require deleting or commenting the previous separator.
In the following example, many different records are presented and various combinations of separators are used:
{
fst: { a=1 },
scd: { i:1, s:'a', trd:{ nme='list'; lst=[1,2,3] }},
// third: "This element is commented out, but previous separator is OK."
}{ fst: { a: 1 }, scd: { i: 1, s: 'a', trd: { lst: [1, 2, 3], nme: 'list' } } }
Record elements are accessed via record selectors. Record
selectors use names to access the elements. The syntax is similar to
tuple selectors, but the dot cannot be used here, instead
‘$’ is used: <record>$<name>,
where <name> is a record element name.
A selector $<name> is applicable to any record
that has an element with the specified name.
{ a:1, b:2, c:3, d:4, e:5 }.f()
where{
f(t) = {#
t$a,
t$a + t$b,
t$a + t$b + t$c,
t$a + t$b + t$c + t$d,
t$a + t$b + t$c + t$d + t$e
#};
}{# 1, 3, 6, 10, 15 #}
The selector names and element types are checked in the type-checking phase, so that it is not possible to use an invalid selector.
.<name>The record selector .<name> is a synonym for
$<name>, with almost the same semantics. The only
difference is that the dot selector has a lower syntactic precedence
than a dot function call. That is, if the dot operator is used in a
context that can be understood as both a function call and a record
selector, then it will be interpreted as a function call.
In short:
r.f(a) is the function call f(r,a);(r.f)(a) is the function call of function
r.f with parameter a;
r$f(a) and
(r$f)(a).Record updaters are operators that replace one of the record elements with a given value. As with the tuple updaters, the given record is not modified - instead, a new record is constructed with the copied and replaced values.
The basic form of the record update selector is:
<record>^<name>(<value>). A simple update
selector ^name(...) can be applied to any record that
contains an element with the specified name.
In an advanced form, a sequence of update selectors can be
used. In this case, the syntax is R^a^b^c(x), which is
semantically equivalent to: R^a( R$a^b( R$a$b^c(x) ) ).
Please note that the expressions R^a^b(x) and
R$a^b(x) are not semantically equivalent. The first
evaluates the record R, whereby the value of R$a$b is
replaced by x. The second evaluates R$a,
whereby the value of the attribute b is replaced by
x.
{#
{ a:1, b:2 }^a(5),
{ a:1, b:{x:2, y:3} }^b^x(5),
{ a:1, b:{x:2, y:3} }$b^x(5)
#}{# { a: 5, b: 2 }, { a: 1, b: { x: 5, y: 3 } }, { x: 5, y: 3 } #}
The record data type is written as follows:
Record[<name1>:<type1>, <name2>:<type2>, ..., <namen>:<typen>].
If R1 is a record type and R2 is a record
type with some elements the same as in the R1 and with no
other elements, then we say that R1 is a subtype
of R2.
If a selector (updater) works with R1, then it must also
work with R2. Then any function that works with
R1 and uses its selectors and updaters will also work with
R2.
In the following example, the function f sums the
elements a and b of a given record. The
function can also be applied to subtypes. It is only necessary that the
argument is a record whose elements a and b
have the same value type.
{#
{ a:1, b:2 }.f(),
{ q:1, b:2, a:3 }.f(),
{ a:1, b:2, s:'a string' }.f(),
{ a:1.2, b:2.4, s:'a string' }.f(),
{ a:'a', b:'b', x:1.2, y:2.4, s:'a string' }.f()
#}
where{
f(t) = t$a + t$b;
}{# 3, 5, 3, 3.5999999999999996, 'ab' #}
If a function or another context allows a record type or its subtype,
the type is usually annotated as follows:
RecordX[name:type]['2], where RecordX stands
for extendable record and '2 represents an
optional record extension.
For example, the types of the $name selector and the
^name updater are:
(RecordX[name:'1]['2] -> '1)
(RecordX[name:'1]['2] * '1 -> RecordX[name:'1]['2])
Tuple and record elements can have functional types. The following example uses a collection of named float functions to compute a table with its values.
apply(
[
{ name:"Id "; fn:\x:x },
{ name:"Sine "; fn:sin },
{ name:"Cosine "; fn:cos },
{ name:"Square "; fn:\x:x*x }
],
[0.0, 0.5, 1.0, 1.5]
)
where{
apply( functions, values ) =
functions
.map(\fn [values]: fn$name + applyFn(fn$fn,values) )
.strJoin( '\n' )
;
applyFn( fn, values ) =
values
.map( fn )
.map( asString )
.strJoin(' ')
;
}Id 0. 0.5 1. 1.5
Sine 0. 0.479425538604203 0.8414709848078965 0.9974949866040544
Cosine 1. 0.8775825618903728 0.5403023058681398 0.0707372016677029
Square 0. 0.25 1. 2.25
Each database use occurs within a single database session, which uses a specific database connection. Simple database programs use a single database session and a single database connection.
The more complex case of programs having multiple database connections is not covered by this tutorial, yet.
In Wafl, programs implicitly connect to a database, based on defined connection parameters. The programmer does not explicitly establish or break the connection between the program and the database.
In command-line programs, connection parameters can be defined in two ways: using command-line arguments and using configuration files. In both cases, to define a connection, it is necessary to specify the driver, database name, username, and password.
Driver is selected using the
-dbdriver:<driver-name> option, where
<driver-name> can be Odbc,
Db2v11, Sqlite and None.
Database is selected using the
-db:<db-alias> option, where
<db-alias> is the name of the database that is
visible to the selected driver. For Sqlib, alias is the
filename of the database file. * Both the driver and the database can be
specified with a single option
-db:<driver-name>:<db-alias>.
Username is specified using the
-user:<username> option. In some cases, and depending
on the database driver and operating system, the username is not
required (for Sqlib) or can be detected from the current
terminal session (Db2).
Password is specified using the
-pwd:<password> option. Both username and password
can be specified by a single option
-user:<username>:<password>.
In command-line programs, it is recommended not to specify a password in the command line, especially if the commands are specified in batch files.
If the program is running in console mode and the password is not entered or is incorrect, the program will prompt the user to enter the correct password three times.
Some of the database connection parameters can be set using
configuration files. These parameters are located in the
[database] section:
default-driver parameter defines the default
database driver that is used if no other driver is explicitly
specified.default-db parameter defines a database name that
is used if no other database is explicitly specified.default-query-type parameter defines whether
queries are typed or untyped by default. It can be typed or
untyped.For security reasons, it is not possible to specify a username or a password in configuration files.
Here is an example of the [database] section in a
configuration file:
[database]
default-db = Test
default-driver = Db2v11
default-query-type = untypedFor more detail please see the Configuration Files section.
The usual way to write database queries in Wafl is to define query functions. Query functions have a custom syntax, but they evaluate in the same way as any other Wafl function.
A query function definition begins with the keyword sequence
[typed | untyped] [sql] query, followed by the query
enclosed in curly braces. The function arguments can be used in the
query using the usual host variable syntax supported by many
database tools.
The keywords typed and untyped specify the
function return type. If neither is specified, the default behavior
depends on the implementation version and the configuration files. It is
strongly recommended to always specify one of these keywords.
In both cases, the result of the query is a list of rows, but the rows can be represented by records (typed rows) or by maps of string values (untyped rows).
The sql keyword specifies that the query is written in
SQL. No other query languages are supported in this version of Wafl.
For example, the following query function returns a list of books with a given title:
qBooks( title ) = untyped sql query {
select * from books
where book_title = :title
};The untyped query function returns a list of string maps:
List[Map[String][String]]. Each value returned by the
database is converted to its string representation and returned as a
string. The processing of rows is the same as the processing of the
lists and string maps map types.
For example, the following code creates the report on books whose
titles contain the given string, in CSV format. Each row is converted to
a line of text, using a lambda function \r:..., and the
rows are glued together using \n as a separator:
booksReport( titlePart ) =
'Title,Author,Pages\n'
+ qBooks( '%' + titlePart + '%' )
.map(\r: r['title']) + ',' + r['author'] + ',' + r['pages'])
.strJoin('\n');
qBooks( titleTpl ) = untyped sql query {
select title, author, pages
from books
where book_title like :titleTpl
};The type of the query function qBooks is:
(String -> List[Map[String][String]]).
The column names are case-sensitive, but the row column names can have different case that the column names specified in the query. That depends on the database used.
Untyped queries all have the same result type. If we need to write some code that handles the results of many different queries in a general way, then it can be better to use untyped queries.
The typed query function returns a list of records, where the records elements correspond to the columns by both name and type. The type of the query result is checked during the type-checking of the program.
For example, the code equivalent to the previous example, but with typed queries, can be written as follows:
booksReport( title ) =
'Title,Author\n'
+ qBooks( title )
.map(\r: r$title + ',' + r$author + r$pages$)
.strJoin('\n');
qBooks( title ) = typed sql query {
select * from books
where book_title = :title
};The query function type is:
(String -> List[Record[author: String, pages: Integer, title:String]].
The elements of record types are usually listed in lexicographical order, but this is not important. They are always accessed by name, never by their position.
Untyped queries are always strongly type-checked in the same way as the other Wafl functions. Thus, they are better to be used when we need to be sure that the data that comes from the database is of the expected and appropriate type.
Query arguments are analyzed and type-checked in the same way as the column types. If the database provides the data on the query parameters, these data is used to determine the argument types. For example, for most of the relational databases and the usual ODBC interface, the following query will have correctly determined arguments:
qBooks( title, pages, author ) = untyped sql query {
select *
from books
where title = :t
or pages = :p
or author = :a
};The type of this function should be determined as:
(String * Integer * String -> List[Map[String][String]]).
Arguments are handled the same way for typed and untyped queries. However, some databases and database drivers may only accept string arguments, regardless of where the arguments are used in the query.
If a query text is not known in advance when the program is developed, then database can be queried using queries defined as strings. In such cases the queries cannot include arguments - the arguments values should be embedded in the query text.
The library function dynSqlQuery gets a query text and
returns a record that contains the query execution status and the
resulting rows. Its type is:
(String -> Record[errCode:Int, errText:String, ok:Bool, result:List[Map[String][String]]]).
ok - a simple logical result, true if
everything is correct;errCode - the standard SQLCODE integer value;errText - the textual representation of the error;result - result, as a list of string maps.When arguments are manually embedded in the queries, this can present a complex security issue!
This kind of queries is called dynamic because the query text is usually dynamically generated before the execution. This has nothing to do with the terms static and dynamic queries the context of relational databases. In this sense, all queries in Wafl are dynamic, because they are all prepared and executed when the program starts.
All query functions use lazy reading - the result of the query is a so-called lazy list. No rows are retrieved from the database before attempting to read the corresponding element of the resulting list. Rows are read as much as are actually needed.
Sometimes it takes very little to mess things up. For example, it’s enough to try to calculate the length of a list that represents the result of a query, and lazy semantics are lost - the only way to accurately calculate the number of rows in the response is to retrieve all the rows.
The transactional functions are a special type of Wafl
functions that have transactional semantics. A transactional function is
essentially defined as a logical conjunction of a sequence of logical
expressions - if all expressions evaluate the logical value
true, then the transaction is committed and the result is
true, otherwise, if at least one expression evaluates to
false, the evaluation is canceled, the transaction is
rolled back, and the result is false.
The definition of a transactional function begins with the keyword
sequence [sql] transaction, followed by the sequence of
action statements enclosed in curly braces and separated by semicolons.
Just like with the queries, SQL is currently the only supported
language.
Three kinds of action statements are supported:
SQL statements, that evaluate to true if
successfully executed and to false in the case of
failure;
regular logical Wafl expressions;
action statements, that modify the environment and
return true if successful.
SQL statements are regular SQL statements with no predefined limits. However, each driver and/or database can have some constraints.
Each driver must support insert, update and delete statements. Additionally, the statements for creating and dropping SQL objects, granting and revoking privileges and other statements can be supported.
The host variables are used in SQL statements in the same way as the queries.
Logical expressions are used as a kind of guards. If a logical
expression evaluates to true, everything is assumed to be
correct and the transaction resumes. If it evaluates to
false, transaction is cancelled and rolled back.
…todo…
The following transactional function insert a row in a book table and updates its pages count:
tAddBook( title, author, pages ) = sql transaction {
insert into books( title, author )
values (:title, :author);
update books
set pages = :pages
where title = :title
and author = :author;
};Tuple elements are accessed via tuple selectors. As tuple elements are not named, their position is used to select them. The positions of the tuple elements are 1-based.
Tuple selectors use the dot-syntax
<tuple>.<n>, where a positive integer indicates
the position of the element to be read.
{# 1, 2, 3, 4, 5 #}.f()
where{
f(t) = {#
t.1,
t.1 + t.2,
t.1 + t.2 + t.3,
t.1 + t.2 + t.3 + t.4,
t.1 + t.2 + t.3 + t.4 + t.5
#};
}{# 1, 3, 6, 10, 15 #}
The selector indices and element types are checked in the type-checking phase so that it is not possible to use an invalid selector.
To support the syntax similar to records, the symbol ‘$’
can be used instead of the dot. The two syntaxes are equivalent -
x$1 is the same as x.1.
Command line programs often require some output to describe the actions of the program, or read and write some files, without all of this being explicitly part of the result that the program computes. Such things are called side effects. They do not exist in pure functional languages, but they can be of great use in command-line programs and scripts.
Some of the functions presented in Command Line Library create side
effects. For example, the input function and all
echo functions. These functions focus on communication, and
controlling the side effects is their secondary task. There are two
functions whose main task is to control the side effects:
return and aside.
Function / Type and Description
return
('1 * '2 -> '2)
Evaluates both arguments, discards the 1st result and returns 2nd one.
aside
('1 * ('1 -> '2) -> '1)
Applies 2nd arg. to 1st, discards the result and returns 1st arg:
aside(x,fn) == fn(x).return(x)
The function return has two arguments. It evaluates both
in the specified order and returns the second one. It is assumed that
the first argument only has a side effect role and its value is
discarded.
It can be misleading to think of this function in the usual form
return(x,y), as it is normally used in a different way. It
is used almost exclusively in the form
...exp... .return(y), where the expression
...exp... is evaluated and discarded and then
y is evaluated and returned. Only the side effects of the
expression ...exp... are important, while the result is
discarded.
"Good morning!"
.return( "Good afternoon!" )Good afternoon!
The function return is useful, for example, after
writing a file, traversing a directory and some other operations with
side-effects, when there is no need to return the specific results. For
example, the following expression performs some processing, then
discards the current result and returns the message “OK” as confirmation
that the program has finished:
... some processing ...
.echoTxt('--------\n')
.return( "OK" )...whatever the processing output is...
--------
OK
"Good morning!"
.echoLn()
.echoTxt( '-------------------\n' )
.return( "Good afternoon!" )Good morning!
-------------------
Good afternoon!
The function return is not always the best solution, as
it not only discards the result of the first argument, but may also
discard an error report. You should therefore use it with caution.
The function aside is similar to return -
it evaluates two arguments in the specified order, but then returns the
first and discards the second. It assumes that the current result is
still needed, but performs some intermediate processing. It is typically
used to output a report on the current progress or result status, as in
the following example:
"Good morning!"
.aside(\x:
("Before saying: \"" + x + "\", bring a cup of coffee first!").echoLn()
)Before saying: "Good morning!", bring a cup of coffee first!
Good morning!
In some simple cases, it is often sufficient (and better) to use
echo functions (the echoFn is perfect in the
previous example), but if the reporting involves a file creation or a
database update, then aside is a much better solution.
The function aside can be used to prepare some temporary
resources for processing or to perform a cleanup after the processing.
For example, consider the following code:
... begin ...
.aside(\x: ... create some tmp files ... )
... processing ...
.aside(\x: ... delete tmp files ... )
... finalize ...Having discussed list processing, we now have a broader picture of common programming techniques in functional programming languages. The computation is implicitly controlled by some higher order functions and by the processed data itself. This approach is general and flexible enough to solve practically any problem. In some cases, however, the creation and processing of additional lists burdens the computation without this really being necessary.
For thos reason, the Wafl core library contains some so-called iterative functions. Iterative functions are used as a tool to control the iterative computation, but based on integers and conditions, rather than on sequences.
Function / Type and Description
iterate
('1 * Int * Int * ('1 * Int -> '1) -> '1)
Left associative folding of specified integers range:
iterate(zero,2,4,fn) = fn(fn(fn(zero,2),3),4) =
zero.fn(2).fn(3).fn(4) = (2..4).foldl(fn,zero)
iterateBy
('1 * Int * Int * Int * ('1 * Int -> '1) -> '1)
Left associative folding of specified integers range by step:
iterateBy(zero,2,6,2,fn) = fn(fn(fn(zero,2),4),6) =
zero.fn(2).fn(4).fn(6) = [2,4,6].foldl(fn,zero)
repeatUntil
('1 * ('1 -> '1) * ('1 -> Bool) -> '1)
Repeats the function evaluation until the condition is met:
repeatUntil(x,fn,cond) = if cond(x) then x else
fn(x).repeatUntil(fn,cond)
The function iterate(zero,from,to,fn) computes a left
associative folding of the specified integer range. It is functionally
equivalent to imperative for-loop, like the next one in C++:
auto result = zero;
for( int i=from; i<=to; i++ )
result = fn( result, i );
return result;The function iterate(zero,from,to,fn) is equivalent to
expression (from..to).foldl(fn,zero), but performs the
computation without creating the list from..to, so that it
is more efficient. If the range is in descending order, the negative
step is used:
{#
iterate( '#', 1, 10, \s,x: s + '-' + x.asString() ),
iterate( '#', 10, 1, \s,x: s + '-' + x.asString() ),
(1..10).map(sum(1,_)),
(1..10).map(fact)
#}
where {
sum(n,m) = iterate( 0, n, m, operator+ );
fact(n) = iterate( 1, 1, n, operator* );
}{# '#-1-2-3-4-5-6-7-8-9-10', '#-10-9-8-7-6-5-4-3-2-1', [1, 3, 6, 10, 15, 21, 28, 36, 45, 55], [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800] #}
The function iterateBy(zero,from,to,step,fn) is a more
general form of iterate. If iterate is used,
the step is always 1 or -1. With iterateBy, the step may be
any integer.
{#
iterateBy( '#', 1, 10, 2, \s,x: s + '-' + x.asString() ),
iterateBy( '#', 10, 1, -3, \s,x: s + '-' + x.asString() )
#}{# '#-1-3-5-7-9', '#-10-7-4-1' #}
The function repeatUntil(x,fn,cond) controls the
iteration based on the given condition. It repeats the iterative
transformation of x until the condition is fulfilled.
Again, it can be useful to describe this in an imperative way:
auto result = x;
while(!cond(result))
result = fn(result);
return result;In the next example, the given number is multiplied by itself until the result is greater than or equal to 1000:
(2..10).map( enlarge )
where {
enlarge(n) = n.repeatUntil(
operator*(n,_),
operator>=(_,1000)
);
}[1024, 2187, 1024, 3125, 1296, 2401, 4096, 6561, 1000]
Function / Type and Description
fileRead
(String -> String)
Reads entire file to a string.
fileReadPart
(String * Int * Int -> String)
Reads a part of the file, from given 0-based position and with given
length in bytes: fileReadPart( fname, pos, bytelen )
fileSize
(String -> Int)
Returns the file size.
The fileRead(filepath) function reads the file and
returns its content as a string. The entire file is read, regardless of
its size. This approach should not be used for very large files due to
the high memory consumption.
For very large files, the function
fileReadPart(filepath,pos,bytelen) should be used. It reads
a part of the file that starts at the given position pos
(in bytes) and has the given length bytelen.
The function fileSize(filepath) returns the size of the
specified file.
fileRead( 'src/txt/en/title.md' )
# Wafl Tutorial
*Wafl* is a strongly typed functional programming language with implicit type inference. It was originally designed as an experimental application of FP languages in web development. A few years later, however, Wafl became a simple and very fast scripting language, with a simple and efficient interface to databases.
The Wafl Tutorial introduces the language constructions, the use of the command line interpreter, elements of the core library and many examples.
Welcome to *Wafl*!
*Saša Malkov*
fileReadPart( 'src/txt/en/title.md', 100, 100 )nference. It was originally designed as an experimental application of FP languages in web developme
fileSize( 'src/txt/en/title.md' )529
Function / Type and Description
fileWrite
(String * String -> String)
Writes string to a file and truncates old content:
fileWrite( fname, content )
fileWriteAppend
(String * String -> String)
Writes string to a file by appending to the file end.
fileWriteAppend( fname, content )
fileWriteTo
(String * String -> String)
Writes string to a file and truncates old content:
fileWriteTo( content, fname )
fileTruncate
(String -> Bool)
Truncates a file. Returns false if truncate fails or file does not exist.
The function fileWrite(filename,s) writes the given
character string s to the file and discards the old file
contents, if present. The function fileWriteTo(s,filename)
does the same, only with the order of the arguments changed. It is good
to have both functions because of the dot-syntax.
If an empty string s is specified, the file is
truncated. If there is no such file, a new empty file is created.
The functions fileWriteAppend and
fileWriteAppendTo have the same arguments, but append the
given string s to the end of the file and do not discard
the previous file contents.
Each of the functions returns the string s as a
result.
The function fileTruncate truncates the file. If the
file does not exist, a new empty file is created. This is exactly the
same behavior as when writing an empty string with
fileWrite or fileWriteTo.
{#
fileWrite( "tmp.txt", "One sentence. " ),
fileWrite( "tmp.txt", "Another sentence. " )
#}{# 'One sentence. ', 'Another sentence. ' #}
fileRead( 'tmp.txt' )Another sentence.
{#
fileWrite( "tmp.txt", "One sentence. " ),
fileWriteAppend( "tmp.txt", "Another sentence. " )
#}{# 'One sentence. ', 'Another sentence. ' #}
fileRead( 'tmp.txt' )One sentence. Another sentence.
Function / Type and Description
fileExists
(String -> Bool)
Checks whether a file with the given name exists.
fileOrDirExists
(String -> Bool)
Checks whether a file or a directory with the given name exists.
fileDelete
(String -> Bool)
Deletes a file.
fileNewTempName
( -> String)
Generates a new temporary file name.
The functions fileExists and
fileOrDirExists check and return whether a file or
directory with the specified name exists.
The function fileExists checks the files and if it
returns true, then there is a file with the given name. It
can be used to check before reading or deleting a file.
{#
fileExists( 'tmp.txt' ),
fileExists( 'tmp2.txt' ),
fileDelete( 'tmp.txt' ),
fileExists( 'tmp.txt' )
#}{# true, false, true, false #}
The function fileOrDirExists checks both the files and
directories and returns true if there is a file or a
directory with the given name. It can be used to check before creating a
file or a directory.
The function fileNewTempName() returns a possible name
for a new temporary file. The file name is located in the directory for
temporary files, which depends on the respective platform. When the
program is terminated, all files with the names returned by this
function are deleted.
Function / Type and Description
dirCreate
(String -> Bool)
Creates a directory.
dirDelete
(String -> Bool)
Deletes a directory, if it is empty
dirExists
(String -> Bool)
Checks whether a directory with the given name exists.
dirFiles
(String -> List[String])
Returns the list of files that fulfill the given filter.
dirSubdirs
(String -> List[String])
Returns the list of subdirs that fulfill the given filter.
fileOrDirExists
(String -> Bool)
Checks whether a file or a directory with the given name exists.
The functions dirFiles(filter) and
dirSubdirs(filter) return lists of files and directories
that match the given filter. The paths returned are relative to the
current working directory.
"Directories:\n "
+ dirSubdirs( '*' ).strJoin('\n ')
+ "\nFiles:\n "
+ dirFiles( '*' ).strJoin('\n ')Directories:
.vscode
bld
code
design
lib
resources
src
Files:
bldall.cmd
bldsite.cmd
pandocHtml.cmd
pandocPdf.cmd
prepmd.cmd
README
run_local_http_server.cmd
site.cmd
test.wafl
todo.notes.txt
Tutorial - Site Version.lnk
Tutorial - Standalone Version.lnk
WaflTutorial.md
The function dirExists is similar to
fileExists and fileOrDirExists, but it only
checks whether a directory with the specified name exists. It can be
used before creating, deleting or traversing a directory.
The regex support in Wafl uses the ECMAScript syntax for regular expressions, as defined and implemented in the C++ standard library from C++11.
By default, all quantifiers are greedy (they match as many characters
as possible). This can be overridden to ungreedy (match as few
characters as possible) by adding a question mark ? after
the quantifier.
Function / Type and Description
regexMatch
(String * String -> Bool)
Checks whether a string matches given regular expression.
regexMatchI
(String * String -> Bool)
Checks whether a string matches given regular expression. Ignores upper and lower case.
The regex matching functions regexMatch(s,r) and
regexMatchI(s,r) check whether the given string
s matches the given regular expression r. The
match is performed for the entire string s. The only
difference is that regexMatch(s,r) ignores upper and lower
case.
{#
s.regexMatch("ab.*AB"),
s.regexMatch(".*ab.*AB.*"),
s.regexMatch(".*ab.*cd.*"),
s.regexMatch(".*AB.*ab.*"),
s.regexMatchI(".*AB.*ab.*")
#}
where {
s = "abcabcABCABC";
}{# false, true, false, false, true #}
Function / Type and Description
regexPos
(String * String -> Int)
Finds the first regex matching position in the given string.
regexPosI
(String * String -> Int)
Finds the first regex matching position in the given string. Ignores upper and lower case.
regexPosAll
(String * String -> List[Int])
Finds all regex matching positions in the given string.
regexPosAllI
(String * String -> List[Int])
Finds all regex matching positions in the given string. Ignores upper and lower case.
The regex functions regexPos(s,r) and
regexPosI(s,r) search for the first position of a substring
of the given string s that matches the given regular
expression r.
{#
s.regexPos("ab.*AB"),
s.regexPos("ab.*cd"),
s.regexPos("AB.*ab"),
s.regexPosI("AB.*ab")
#}
where {
s = "abcabcABCABC";
}{# 0, -1, -1, 0 #}
The regex functions regexPosAll(s,r) and
regexPosAllI(s,r) search for the starting positions of all
substrings of the given string s that match the given
regular expression r.
{#
s.regexPosAll("ab.*AB"),
s.regexPosAll("ab.*cd"),
s.regexPosAll("AB.*ab"),
s.regexPosAllI("AB.*ab"),
s.regexPosAllI("AB.*?ab"),
s.regexPosAllI("a.*c"),
s.regexPosAllI("a.*?c")
#}
where {
s = "abcabcABCABC";
}{# [0], [], [], [0], [0, 6], [0], [0, 3, 6, 9] #}
Function / Type and Description
regexReplace
(String * String * String -> String)
Replaces all matching of a regex with the given string.
regexReplaceI
(String * String * String -> String)
Replaces all matching of a regex with the given string. Ignores upper and lower case.
The regex functions regexReplace(s,r,x) and
regexReplaceI(s,r,x) search for the positions of all
substrings that match the given regular expression r, and
replace them with the given string x.
{#
s.regexReplace("ab.*AB", "#"),
s.regexReplace("ab.*?AB", "#"),
s.regexReplace("ab.*cd", "#"),
s.regexReplace("AB.*ab", "#"),
s.regexReplaceI("AB.*ab", "#"),
s.regexReplaceI("AB.*?ab", "#"),
s.regexReplaceI("a.*c", "#"),
s.regexReplaceI("a.*?c", "#")
#}
where {
s = "abcabcABCABC";
}{# '#C', '#CABC', 'abcabcABCABC', 'abcabcABCABC', '#C', '#c#C', '#', '####' #}
If it is necessary to preserve a part of the found substring, you can
use the capture groups. A capture group is a part of the regular
expression enclosed in parentheses, except when the first character in
the parentheses is a question mark. In the replacement string
x, you can use $n to copy a capture group,
where n is an integer 1-based index of the corresponding
capture group. In the following example, the capture groups are used to
insert a dash - between two lower case letters:
{#
s.regexReplace("([a-z])([a-z])", "$1-$2"),
s.regexReplace("([a-z])([a-z])", "$1-$2")
.regexReplace("([a-z])([a-z])", "$1-$2")
#}
where {
s = "abcabcABCABC";
}{# 'a-bc-ab-cABCABC', 'a-b-c-a-b-cABCABC' #}
Function / Type and Description
regexSearch
(String * String -> List[String])
Searches for the regex matching in the given string.
regexSearchI
(String * String -> List[String])
Searches for the regex matching in the given string. Ignores upper and lower case.
regexSearchAll
(String * String -> List[List[String]])
Searches for all regex matchings in the given string.
regexSearchAllI
(String * String -> List[List[String]])
Searches for all regex matchings in the given string. Ignores upper and lower case.
The regex search functions regexSearch(s,r) and
regexSearchI(s,r) search for the first substring of the
given string s that matches the given regular expression
r and return all matched sub-elements. The returned value
is a list of strings. The first element of the list is the complete
matched substring, and the following elements correspond to the
sub-patterns (capture groups) of the regular expression.
The following example searches for the pattern
<name>=<n>:
{#
s.regexSearch("([a-z]+)\\s*=\\s*([0-9]+)")
#}
where {
s = "a=12 ;bcd = 24; ef =354";
}{# ['a=12', 'a', '12'] #}
The first element of the returned list is the first matching
substring. The second element is the corresponding part
name (the first part of the regular expression enclosed in
the parentheses ([a-z]+)), and the third element is the
corresponding part n (the second part of the regular
expression enclosed in the parentheses ([0-9]+)).
The functions regexSearchAll and
regexSearchAllI do the same, but for all matching
substrings. The result is therefore not a single list, but a list of
string lists - one list for each matching substring. The elements of the
lists are the same as in the result of the regexSearch
function:
{#
s.regexSearchAll("([a-z]+)\\s*=\\s*([0-9]+)")
#}
where {
s = "a=12 ;bcd = 24; ef =354";
}{# [['a=12', 'a', '12'], ['bcd = 24', 'bcd', '24'], ['ef =354', 'ef', '354']] #}
The functions for command line support are specific to the command line Wafl interpreter. They are not normally available in other versions. The main purpose is to support the read and write operations of the command line terminal.
The command line program interpreter supports the command line program arguments. There are some functions and special operators for working with command line arguments:
Function / Type and Description
$*
List[String]
Returns the list of command line
arguments.
$#
Int
Returns a number of command line arguments.
$ A
(Int -> String)
Return a command line argument:
$1 = cmdLineArgs()[1]
cmdLineArgs
( -> List[String])
Return a list of command line arguments.
cmdLineArgsCount
( -> Int)
Return a number of command line arguments.
The constant operator ‘$*’ is used to get the list of
all command line arguments. The synonym, in the form of a function, is
the function cmdLineArgs(). For example, if a program is
started with the following command:
clwafl program.wafl a b c "abcde"
both $* and cmdLineArgs() return the
following list:
['program.wafl', 'a', 'b', 'c', 'abcde']
The number of arguments can be computed as the size of the list of
command line arguments (size($*)) or by using the constant
operator $#, or its synonym function
cmdLineArgsCount().
Command line programs return an integer code to the caller (operating system or an application). If everything is OK, console programs are expected to return zero. In the event of an error, the programs return some program-specific positive values.
The function cmdSetExitCode(s,code) sets the program
exit code to the specified integer code and returns
s.
Function / Type and Description
cmdSetExitCode
('1 * Int -> '1)
Set program exit code to second argument and return the first one.
if $# > 1
then $1
else "No argument" .cmdSetExitCode(1)No argument
The output to the command line terminal is provided by the echo functions.
Function / Type and Description
echo
('1 -> '1)
Write the argument’s string representation to the console and return
the argument: echo(x) == echoTxt( x, x.asString() )
echoLn
('1 -> '1)
Write the argument’s string representation and new line to the
console and return the argument:
echoLn(x) == echoTxt( x, x.asString() + '\n' )
echoTxt
('1 * String -> '1)
Writes the second argument to the console and return the first argument.
echoFn
('1 * ('1 -> String) -> '1)
Apply the function to the 1st arg. and write the result to the
console: echoFn( x, fn ) == echoTxt( x, fn(x) )
echoOff
('1 -> '1)
Disables console output for the expression. Only the
input function can print the prompt.
echoSilent
('1 -> '1)
Completely disables console output for the expression. Even the
input function cannot print a prompt.
echoCtrl
('1 * String -> '1)
Writes the second argument to the console and return the first argument, but only if the console terminal supports controls codes.
conSupportsCtrlCodes
( -> Bool)
Checks whether console output supports control codes.
Each of the echo functions outputs something to the standard console output and returns its first argument.
The function echo(x) outputs the given value
x converted to a string and returns the original value
x. The function echoLn(x) does the same, but
outputs an additional new line symbol at the end.
"This is a string"
.echo()
.echoLn()This is a stringThis is a string
This is a string
The functions echoTxt and echoFn write some
other strings to the command line output, but return the first argument
in the same way as echo and echoLn. The
function echoTxt(x,s) writes s and returns
x. The function echoFn(x,fn) writes
fn(x) and returns x:
"This is a string"
.echoTxt("Output from echoTxt\n")
.echoFn(\x: 'Output "' + x + '" from echoFn\n' )Output from echoTxt
Output "This is a string" from echoFn
This is a string
The functions echoOff(x) and echoSilent(x)
disable console output for the given expression or the given function,
and evaluate it. The echoOff disables all
echo* functions output. The echoSilent does
the same but it even disables the input function prompt
printing.
{#
[1,2,3].map(\f: f$.echoLn() ),
[4,5,6].map(\f: f$.echoLn() ).echoOff(),
[7,8,9].map( echoOff(\f: f$.echoLn()) )
#}1
2
3
{# ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'] #}
The function echoCtrl(x,s) is very similar to
echoTxt(x,s). The only difference is that the
echoCtrl echoes the string s only if the
output stream can handle ASCII console control codes (i.e. escape
sequences). The tutorial builder runs the following example and
redirects the output to a file, so the control codes are not handled
properly and ehoCtrl will not output anything:
{#
'OK'
.echoTxt('This echo is working...')
.echoCtrl('...and this one is disabled...'),
conSupportsCtrlCodes()
#}This echo is working...{# 'OK', false #}
Using the function echoCtrl is a right way to develop
console programs that use a rich output by using ANSI control codes.
However, in some cases the string to be printed are to be build by a
more complex programs. The creation of formatted output may need some
info on whether the ANSI codes are supported or not. That is provided by
the function conSupportsCtrlCodes(). It returns
true if the output console stream can handle ANSI control
codes.
Using the echoCtrl function is the right way to develop
console programs that use rich output using ANSI control codes. However,
in some cases, the string to be printed needs to be constructed using
some more complex algorithm, and it may be necessary to know whether
ANSI codes are supported or not. This is provided by the
conSupportsCtrlCodes() function. It returns
true if the console output stream can handle ANSI control
codes, and false otherwise.
Please consult documentation on the ConsoleColors.wlib
library and some other info on ANSI console control codes.
Function / Type and Description
input
(String -> String)
Write a string to the console, wait for input and return it.
The function input(s) writes the given string
s to the command line output, waits for the user to enter a
line into the console, and returns the entered string.
"Hello "
+ input( "What is your name? " )
+ "!"What is your name? P.A. Marvin Hello P.A. Marvin!
Function / Type and Description
cmdExecute
(String -> String)
Execute the command in the active console. Shows single-line output and supports the input. Returns the entire output as a result.
cmdShellExecute
(String -> String)
Execute the command in the active console. Shows all outputs and supports the input. Returns the entire output as a result.
cmdLastError
( -> Int)
Get integer code of last command status.
The functions cmdExecute(cmd) and
cmdShellExecute(cmd) execute the given shell command string
and return the results. They return the entire command output as a
result. Both functions support the console input. The difference between
the two functions is that cmdShellExecute sends the entire
output to the console output, while cmdExecute outputs just
a single lines one over another.
The function cmdLastError() returns the exit code of the
last shell command executed. As usual, if everything is OK, it returns
zero.
The HTTP and web functions of the Wafl library are divided in two parts:
Function / Type and Description
httpGetSize
(String -> Int)
Get WWW content length using HTTP/HTTPS HEADER method. [Deprecated.
Use Curl library.]
httpGet
(String -> String)
Get WWW content using HTTP/HTTPS GET method. [Deprecated. Use
Curl library.]
httpGet_callback
(String * (Int * Int -> Int) -> String)
Get WWW content using HTTP/HTTPS GET method, with progress callback.
[Deprecated. Use Curl library.]
The httpGet(uri) function sends a GET HTTP
request with a given uri, waits for a response and returns
the content of the response. It can be used for both HTTP and HTTPS
requests.
httpGet( 'https://wafl.malkov.dev/' )
.size()17416
The function httpGetSize(uri) sends a
HEADER HTTP request with a given uri, waits
for a response and returns the reported size of the response content. It
can be used for both HTTP and HTTPS requests. If a server does not
respond to the HEADER request, the function returns -1. If a server
responds to the request but returns an invalid header or a header
without content size information, the function returns -2.
httpGetSize( 'https://wafl.malkov.dev/' )17416
The httpGet_callback(uri,fn) function sends a
GET HTTP request with a given uri, uses the
fn function of type (Int * Int -> Int) to
process intermediate callbacks, waits for a response and returns the
reported response content. The result value should be the same as in the
case of httpGet. The function
fn(received,expected) can perform some intermediate
processing, such as progress reporting. The argument
received is the total number of bytes received, and the
argument expected is the total number of bytes expected.
The expected should match the result of the
httpGetSize(uri) and can be a negative value if the server
does not deliver the expected size.
httpGet_callback(
'https://wafl.malkov.dev/',
\r,t: t.echoTxt( r.asString() + ' / ' + t.asString() + '\n')
)
.size()7840 / 17416
17416 / 17416
17416
httpGet_callback(
'https://wafl.malkov.dev/',
\r,t: t.echoTxt( r.asString() + ' / ' + t.asString() + '\n')
)
.size()7840 / 17416
17416 / 17416
17416
Function / Type and Description
Form
( -> Map[String][String])
Get form variable set.
FormValue
(String -> String)
Get value of a given form variable.
Session
( -> Map[String][String])
Get session variable set.
SessionValue
(String -> String)
Get value of a given session variable.
Service
( -> Map[String][String])
Get service variable set.
ServiceValue
(String -> String)
Get value of a given service variable.
The functions Form(), Session() and
Service() return the corresponding web variable sets - the
current form content received from a client, the current set of session
variables and the current service configuration. The functions
FormValue(varname), SessionValue(varname) and
ServiceValue(varname) return the value of a variable with
the given variable name.
Function / Type and Description
httpHost
( -> String)
Get HTTP host for current request.
httpPathInfo
( -> String)
Get HTTP path info for current request.
httpScript
( -> String)
Get HTTP script for current request.
The functions httpHost(), httpPathInfo()
and httpScript() return the corresponding parts of the
currently processed URI.
Function / Type and Description
ask
(String -> Map[String][String])
Ask a question by sending the given page content to the client. Returns the next client’s request.
answerAction
( -> String)
Automatic action URI generator for forms in questions.
The ask(pageContent) function sends the given page
content to the client and waits for the returned answer. All links and
form actions on the page that begin with a relative link generated by
answerAction(), represent the alternative answers
for the given page and return to this specific function. The
answerAction() function returns a result that identifies
the specific current program evaluation so that the program can continue
after an appropriate answer is received from the client.
Wafl has two functions for exporting Wafl values to JSON.
Function / Type and Description
toJSON
('1 -> String)
Converts a value to a string in JSON format.
toFmtJSON
('1 -> String)
Converts a value to a string in pretty JSON format.
Both functions can be applied to any Wafl value type. The function
toJSON(x) converts the value x into the JSON
format in the as compact a form as possible. This is useful if the
exported string only needs to be processed by machine.
The function toFmtJSON(x) does essentially the same
thing, but exports the value in a better formatted JSON format. This is
better if the exported string needs to be processed by humans.
(1..5).map(\n:{
n: n,
s: 's'+n$,
t: {# n, 's'+n$ #}
})
.toJSON()[{'n':1,'s':'s1','t':[1,'s1']},{'n':2,'s':'s2','t':[2,'s2']},{'n':3,'s':'s3','t':[3,'s3']},{'n':4,'s':'s4','t':[4,'s4']},{'n':5,'s':'s5','t':[5,'s5']}]
(1..5).map(\n:{
n: n,
s: 's'+n$,
t: {# n, 's'+n$ #}
})
.toFmtJSON()[ {
'n': 1,
's': 's1',
't': [
1,
's1'
]
}, {
'n': 2,
's': 's2',
't': [
2,
's2'
]
}, {
'n': 3,
's': 's3',
't': [
3,
's3'
]
}, {
'n': 4,
's': 's4',
't': [
4,
's4'
]
}, {
'n': 5,
's': 's5',
't': [
5,
's5'
]
} ]
Function / Type and Description
cmdWafl
(String * List[String] -> String)
Run Wafl program file in a command line shell with given arguments and return the result. All arguments are passed to Wafl program. No arguments apply to interpreter. The output is redirected to the caller’s output stream.
cmdWaflSrc
(String * List[String] -> String)
Run Wafl program source in a command line shell with given arguments and return the result. All arguments are passed to Wafl program. No arguments apply to interpreter. The output is redirected to the caller’s output stream.
cmdWaflX
(String * RecordX[]['1] -> String)
Run Wafl program file in a command line shell with given options and
return the result. Options are specified as a record:
cmdLineArgs - the list of command line arguments to pass to
the program (List[String]); coutFile - the
full filename of the output redirection file; if ‘null’ is
specified, output is discarded; if empty string is specified (default),
the caller’s output stream is used .
These functions carry out the evaluation of Wafl programs. The
function cmdWafl(prgpath,args) evaluates a Wafl program in
the specified file. The function cmdWaflSrc(src,args)
evaluates the Wafl program written in the string. Both functions send
the specified arguments as command line arguments to the evaluated
program and return the program output and the result as a return
value.
cmdWaflSrc('2+3',[])5
"A controlled error:\n"
+ cmdWaflSrc( 'an error',[])A controlled error:
--- Loading from command line
Parser error
Unexpected text at the end of the file! [err 1201:6]
Line 1: an error
___/
--- End loading from command line
Each Wafl program evaluation is performed in a separate evaluation environment, but in the same process. This means that most errors do not propagate to the parent program (the one in which one of these functions is used), but if the memory usage becomes too high or the process fails, this can have an impact on the parent program.
It is even possible that such an evaluated program evaluates another Wafl program:
cmdWaflSrc( 'cmdWaflSrc("2+3",[])',[])5
Parallelization is the evaluation of program parts in several threads, i.e. in parallel. Since each Wafl program represents an expression, parallelization requires the program to be segmented into many sub-expressions that are evaluated in parallel.
Only pure functional expressions can be parallelized. If a non-pure expression is parallelized, its result or its side-effects could be different for different evaluations.
The current Wafl implementation does not check whether the object of parallelization is a pure functional expression or not. The developers must therefore take care of this.
Wafl supports explicit and implicit parallelization.
Explicit parallelization is based on the explicit use of parallel versions of some Wafl library functions. These functions are implemented to split the job into parts and to use many threads to complete the evaluation.
The explicit parallel functions include:
map_par - A parallel version of the map
function.mapIdx_par - A parallel version of the
mapIdx function.filter_par - A parallel version of the
filter function.fold_par - A parallel version of the fold
function.newArrayFn_par - A parallel version of the
newArrayFn function.Each of the parallel versions of the functions has the same type and returns the same result as the sequential version, but is evaluated in many parallel threads.
Implicit parallelization assumes that some parts of the programs are implicitly (automatically) parallelized by the interpreter. An important module of the Wafl interpreter is a parallelization engine. Its task is to analyze the available data about the expressions and decide whether an expression should be evaluated sequentially or in parallel. It also decides on many of the parallelization options.
Simplified, one can say that the parallelization engine tries to evaluate the expected benefits of parallelization, taking into account the following:
Based on the evaluated benefits, the engine compares them with a threshold and decides whether parallelization should be used or not. The threshold value can be configured.
By default, the implicit parallelization is disabled. The main reason for this is to avoid errors during the implicit parallelization of non-pure expressions.
In the future, after the interpreter is upgraded to check the purity of expressions, the implicit parallelization might be enabled by default.
The following functions can be implicitly parallelized:
mapmapIdxfoldfilternewArrayFn.Whether a particular instance of these functions is parallelized or not is decided by the parallelization engine. Currently, the parallelization engine decides based on the size of the data collection to be processed, and on the number of parallel jobs currently active.
Sometimes it is useful to have implicit parallelization, but to suggest to the parallelization engine that a part of the code should be evaluated in parallel, if possible. Such a suggestion has the effect of lowering the threshold for the parallelization decision.
To suggest that a program segment should be parallelized, there is a
function parallel. Its type is ('1 -> '1)
and its semantics is almost like the identity function
\x:x. The only difference is that the threshold for
parallelization is lowered.
It can be applied to any expression, but has different effects when applied to functions.
In the general case, if the bound expression is not a simple function
reference, as in parallel(map(lst,fn)), the following
semantics are applied:
So the main difference is whether the arguments are evaluated with parallelization enabled or not.
On the other hand, if the argument is a function, as in
parallel(map)(lst,fn), the parallelization suggestion is
strictly bound to the given function. The main difference is that the
parallelization threshold for the function’s arguments is not lowered.
We could describe the semantics as follows:
If the specified function is a library function like
map, the result is almost the same as using a specialized
parallel map implementation. On the other hand, if the specified
function is a user-defined function, then the result is the same as
defining the user-defined function with parallel.
For example, if we have:
f(x,y,z) = ...defexp...;
then we can suggest its parallelization in two ways:
if we want to parallelize only a selected application, then we
should use parallel(f)(a,b,c), and
if we want to parallelize all function applications, we should
use parallel for the entire expression of the function
definition:
f(x,y,z) = parallel(...defexp...);An alternative is to define a new function parallel_f as
follows:
parallel_f = parallel(f);
The configuration of the parallelization engine can be set via command line options:
-pd, -parallel-disable
-pa, -parallel-auto
-ps, -parallel
-pa, but this mode is biased towards parallelization by
using a lower threshold.-pf, -parallel-force
When implicit parallelization is used, it is possible that the parallelization engine decides to parallelize a part of the program that is not allowed to be parallelized (usually because it is not purely functional). To prevent this, there are explicit sequential versions of functions that prevent implicit parallelization.
There are sequential versions of every implicitly parallelizable function:
map_seq - A sequential version of the map
function.mapIdx_seq - A sequential version of the
mapIdx function.filter_seq - A sequential version of the
filter function.fold_seq - A sequential version of the
fold function.newArrayFn_seq - A sequential version of the
newArrayFn function.The functions foldl and foldr are always
sequential, therefore fold_seq corresponds to the function
fold. It is a special case of foldl and
foldr where an associative function is used.
The key word in parallelization is isolation. The prerequisite for parallel evaluation of some tasks is that they are well isolated from each other. In some concurrent environments, we expect the parallel tasks to communicate with each other to ensure a synchronization. However, in a functional context, especially for purely functional tasks, synchronization is usually not required. All we need is isolation of the tasks.
The main problem with task isolation is data sharing. It is very inconvenient if one task changes some data that the other task is processing at the same time. In a purely functional environment, we do not have such problems. Even in mixed environments, as is the case with Wafl, we usually do not have such problems. As long as we process the data in memory and do not change it, we can split the data into chunks and process the chunks in multiple threads quite safely and in full isolation.
In a few cases, problems can arise, and these are the cases we need to watch out for:
We have already discussed the functions that can work in parallel, as
well as theirs sequential versions and the versions that permit the
implicit parallelization. Most of them, with the exception of the
fold_par function (and of course fold),
normally perform some independent operations on independent data. Only
in the case of fold_par the order of computation can be
important. In other cases, we can say that we have complete isolation
and safe parallelization as long as we do not create side effects. For
this reason, we pay additional attention to folding.
Not every sequence fold can be parallelized. For example, consider the following expression:
(1..20).foldr(
\x,l: if x%2=0 then x:l else l,
[]
)[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
The expression folds an integer sequence by evaluating a filtered
list of even integers. (Of course, we can do this more simply with
filter.) This is an example of fold that cannot be
implemented with foldl with the same arguments, since
foldl expects its first argument to be an integer and the
second argument to be a list of integers.
In order to be able to use both foldl and
foldr with the same result in the general case, two
important conditions must be met:
('1 * '1 -> '1), i.e. it must represent a binary
operator on a domain; anda, b and c the following must
hold: f(a,f(b,c)) == f(f(a,b),c).If we want the fold to be evaluated in parallel, there is an additional condition:
zero must really be a neutral value
for the given operation, as it can be used several times.The following functions are intended for such cases:
Function / Type and Description
fold
(Sequence['2]['1] * ('1 * '1 -> '1) * '1 -> '1)
Returns the sequence fold by an associative function:
fold == foldl == foldr
fold_seq
(Sequence['2]['1] * ('1 * '1 -> '1) * '1 -> '1)
Non-parallel fold of sequence by an associative function:
fold_seq == foldl == foldr
fold_par
(Sequence['2]['1] * ('1 * '1 -> '1) * '1 -> '1)
Folds the sequence elements in parallel. [‘fn’ must be associative]:
[‘zero’ must be ZERO, because it can be used many times]:
fold_par(seq,fn,zero) == foldl(seq,fn,zero) == foldr(seq,fn,zero)
These functions assume that the fold is performed by a given binary
operator of type ('1 * '1 -> '1), which is an
associative operator, and return the same result as foldl.
The function fold_par assumes that the zero
value is a neutral value for the operator.
However, the interpreter does not check whether the operator is
associative or not and if the specified zero value is
really the zero for the operator. So, please use these
functions with caution.
The base function here is fold. It can be implicitly
parallelized. However, if parallelization is required, then
fold_par should be used, and if parallelization is to be
prevented, then fold_seq should be used.
In the following examples, the fold is performed with an operator that returns the sum of the arguments, but additionally outputs the arguments to the console:
(1..50).fold_seq(
\x,y: x + y
=> echoTxt( (if x=0 then '0 ' else ' ') + '+ ' + y$ ),
0
).echoTxt(' =\n')0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 40 + 41 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 50 =
1275
(1..50).fold(
\x,y: x + y
=> echoTxt( (if x=0 then '0 ' else ' ') + '+ ' + y$ ),
0
).echoTxt(' =\n')0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 40 + 41 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 50 =
1275
(1..50).fold_par(
\x,y: x + y
=> echoTxt( (if x=0 then '0 ' else ' ') + '+ ' + y$ ),
0
).echoTxt(' =\n')0 + 1 + 2 + 3 + 4 + 5 + 60 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 290 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 380 + 41 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 500 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 30 + 39 + 40 + 7 + 8 + 9 + 100 + 55 + 155 + 255 + 355 + 455 =
1275
The result is always the same, but in the case of parallel evaluation, the order of operations may be different. After each thread has folded its part of the sequence, the results of the threads are also folded. The zero is used once more than the number of threads.
As already mentioned, we can use command line options to force
(-pf) or prevent parallelization(-pd). We can
also use the functions that always evaluate in parallel as
map_par or sequentially as map_seq. There are
two additional functions that control the implicit parallelization:
Function / Type and Description
parallel
('1 -> '1)
Marks an expression to be parallelized, if possible.
sequential
('1 -> '1)
Force the expression to run sequentially.
The function parallel(exp) suggests to the interpreter
that the specified expression should be evaluated in parallel. It does
not enforce parallel evaluation, but lowers the threshold for
parallelization of the specified expression. There are two versions of
this function:
In the following examples, the same expression is evaluated three times. The first example is as usual, with standard implicit parallelization:
(1..100).fold(
\x,y: x + y
=> echoTxt( (if x=0 then '0 ' else ' ') + '+ ' + y$ ),
0
).echoTxt(' =\n')0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 40 + 41 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 50 + 51 + 52 + 53 + 54 + 55 + 56 + 57 + 58 + 59 + 60 + 61 + 62 + 63 + 64 + 65 + 66 + 67 + 68 + 69 + 70 + 71 + 72 + 73 + 74 + 75 + 76 + 77 + 78 + 79 + 80 + 81 + 82 + 83 + 84 + 85 + 86 + 87 + 88 + 89 + 90 + 91 + 92 + 93 + 94 + 95 + 96 + 97 + 98 + 99 + 100 =
5050
In the next example, we use parallel(fold) to suggest
that the fold function should be evaluated in parallel:
(1..100).(parallel(fold))(
\x,y: x + y
=> echoTxt( (if x=0 then '0 ' else ' ') + '+ ' + y$ ),
0
).echoTxt(' =\n')0 + 1 + 2 + 3 + 4 + 5 + 60 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 300 + 410 + 61 + 62 + 63 + 64 + 65 + 66 + 67 + 68 + 69 + 700 + 11 + 12 + 130 + 71 + 72 + 73 + 74 + 75 + 76 + 77 + 78 + 79 + 800 + 91 + 92 + 93 + 94 + 95 + 96 + 97 + 98 + 99 + 1000 + 81 + 82 + 83 + 84 + 85 + 86 + 87 + 88 + 89 + 90 + 7 + 8 + 9 + 100 + 51 + 52 + 53 + 54 + 55 + 56 + 57 + 58 + 59 + 60 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 50 + 14 + 15 + 16 + 17 + 18 + 19 + 200 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 400 + 55 + 155 + 255 + 355 + 455 + 555 + 655 + 755 + 855 + 955 =
5050
And finally, by applying parallel to an expression, we
suggest that the entire expression should be evaluated in parallel:
(1..100).fold(
\x,y: x + y
=> echoTxt( (if x=0 then '0 ' else ' ') + '+ ' + y$ ),
0
).echoTxt(' =\n').parallel()0 + 10 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 400 + 510 + 410 + 21 + 52 + 53 + 54 + 55 + 56 + 57 + 58 + 59 + 600 + 81 + 82 + 83 + 84 + 85 + 86 + 87 + 88 + 89 + 900 + 11 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 500 + 91 + 92 + 93 + 94 + 95 + 96 + 97 + 98 + 99 + 1000 + 71 + 72 + 73 + 74 + 75 + 76 + 77 + 78 + 79 + 800 + 61 + 62 + 63 + 64 + 65 + 66 + 67 + 68 + 69 + 70 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 100 + 55 + 155 + 255 + 355 + 455 + 555 + 655 + 755 + 855 + 955 =
5050
Recall that <exp>.parallel() is equivalent to
parallel(<exp>), so the parallel
function in the previous example was applied to the entire previous
expression.
Implicit parallelization depends on many properties of the specific interpreter implementation and the specific platform. Therefore, it is not always possible to repeat the given examples with the same degree of parallelization. The consequence is that it is not always possible to predict how an expression will be evaluated.
For this reason, we will not go into the results of the previous examples in detail. We expect that the first example was not evaluated in parallel and that the other two examples were evaluated in parallel, but we cannot be sure. Try changing the list size to see the different results.
The function sequential(exp) is another function for
controlling the implicit parallelization. This function forces the
sequential evaluation of the given expression. During the evaluation of
the given expression, only the explicitly parallel functions such as
map_par will work in parallel and all other computation
will be performed sequentially.
As with parallel, there are two versions of
sequential function:
The sequential function is stronger than the
parallel function. While parallel suggests the
parallelization, sequential forces sequential evaluation.
Any use of parallel in expressions bound by
sequential is ignored. On the other hand, the use of
sequential in expressions bound by the
parallel has the expected effect.
In the following examples, we compute (almost) the same thing in different ways. Some of the expressions will always work sequentially, some will always work in parallel and the others will depend on the program options:
{#
'Always sequential:',
fold_seq.test(),
fold.sequential().test(), // sequential( fold )
fold.test().sequential(), // sequential(...exp...)
fold.test().sequential().parallel(),
fold.test().parallel().sequential(),
fold.sequential().parallel().test(),
fold.parallel().sequential().test(),
'Depending on the options:',
fold.test(),
fold.parallel().test(), // parallel( fold )
fold.test().parallel(), // parallel(...exp...)
'Always parallel:',
fold_par.test()
#}
where {
test( foldFn ) =
if (1..1000).foldFn( operator+, 1 ) = 500501
then 'seq' else 'par';
}{# 'Always sequential:', 'seq', 'seq', 'seq', 'seq', 'seq', 'seq', 'seq', 'Depending on the options:', 'seq', 'par', 'par', 'Always parallel:', 'par' #}
We assume that the sequential fold works in a single thread and uses
a single initial value 1. The result is therefore the sum
of the integers in the range 1..1000 plus 1,
i.e. 500501. If, on the other hand, the fold is evaluated
in parallel, the initial value 1 is added many times and
the result is greater than 500501.
A large part of the Wafl core library has already been presented in the previous chapters. Here we present a more complete reference of the Wafl library in a compact form. The functions and operators are listed in alphabetical order and organized by main sections. For each function, its type and a short description are specified.
This chapter is mostly generated automatically, by processing the available information embedded in the Wafl interpreter, binary libraries and Wafl libraries. As a consequence, there may exist some formatting issues.
Wafl Core Library consists of the functions embedded in the Wafl
interpreter. The Wafl interpreter can list all supported functions and
operators. To get a compact list, use the option -listlib
or a shorthand ?:
clwafl -listlib
clwafl ?
For more detailed report, add the option -verbose or a
shorthand ??:
clwafl -listlib -verbose
clwafl ??
To find all functions that have the given substring (in the following
example - str) in their name, add the substring to the
command line:
clwafl -listlib:str
clwafl ? str
or:
clwafl -listlib:str -verbose
clwafl ?? str
For further details please consult the Command Line Reference.
Function / Type and Description
! A
(Bool -> Bool)
Negates the given logical value.
- A
(Numeric['1] -> Numeric['1])
Unary negation operator.
A != B
('1 * '1 -> Bool)
Not-equal-to operator.
A % B
(Int * Int -> Int)
Integer remainder operator.
A %% B
(Int * Int -> Int)
Integer modulus operator. Always positive.
A & B
(Int * Int -> Int)
Bitwise AND operator.
A && B
(Bool * Bool -> Bool)
Boolean conjunction operator.
A * B
(Numeric['1] * Numeric['1] -> Numeric['1])
Binary subtraction operator.
A ** B
(Numeric['1] * Numeric['1] -> Numeric['1])
Power operator: A ** B = A to the power of B
A + B
(Value['1] * Value['1] -> Value['1])
Binary addition operator.
A - B
(Numeric['1] * Numeric['1] -> Numeric['1])
Binary subtraction operator.
A / B
(Numeric['1] * Numeric['1] -> Numeric['1])
Binary division operator.
A < B
(Value['1] * Value['1] -> Bool)
Less-than operator.
A << B
(Int * Int -> Int)
Bitwise left shift operator.
A <= B
(Value['1] * Value['1] -> Bool)
Less-than-or-equal-to operator.
A == B
('1 * '1 -> Bool)
Equal-to operator.
A > B
(Value['1] * Value['1] -> Bool)
Greater-than operator.
A >= B
(Value['1] * Value['1] -> Bool)
Greater-than-or-equal-to operator.
A >> B
(Int * Int -> Int)
Integer right shift operator.
A | B
(Int * Int -> Int)
Bitwise OR operator.
A || B
(Bool * Bool -> Bool)
Boolean disjunction operator.
EncodeUrlQuery
(Map[String][String] -> String)
Encodes variable set for HTTP query string.
JoinValueSets
(Map[String][String] * Map[String][String] -> Map[String][String])
Joins two variable sets.
RemoveFromValueSet
(Map[String][String] * String -> Map[String][String])
Removes a variable with the given name from the variable set.
UpdateValueSet
(Map[String][String] * String * String -> Map[String][String])
Updates the variable set with given var. name and var. value.
abs
(Numeric['1] -> Numeric['1])
Absolute value.
acos
(Float -> Float)
Inverse cosine function.
aside
('1 * ('1 -> '2) -> '1)
Applies 2nd arg. to 1st, discards the result and returns 1st arg:
aside(x,fn) == fn(x).return(x)
asin
(Float -> Float)
Inverse sine function.
atan
(Float -> Float)
Inverse tangent function.
atan2
(Float * Float -> Float)
Inverse tangent function: atan2(y,x) = atan(y/x)
between
(Numeric['1] * Numeric['1] * Numeric['1] -> Bool)
Checks whether the 1st arg. lies between 2nd and 3rd arguments.
x.between(a,b) == x >= a and x <= b
cos
(Float -> Float)
Cosine function.
debugIsOn
( -> Bool)
Checks whether the debug mode is turned ‘on’.
debugOff
('1 -> '1)
Turns debug mode ‘off’ and returns the argument.
debugOn
('1 -> '1)
Turns debug mode ‘on’ and returns the argument.
debugState
( -> String)
Evaluates the debug state string.
dynSqlCommand
(String -> Bool)
Executes SQL command given in a string dynamically.
dynSqlQuery
(String -> Record[errCode:Int, errText:String, ok:Bool,
result:List[Map[String][String]]]) Executes SQL query
given in a string dynamically.
exp
(Float -> Float)
Exponential function.
iterate
('1 * Int * Int * ('1 * Int -> '1) -> '1)
Left associative folding of specified integers range:
iterate(zero,2,4,fn) = fn(fn(fn(zero,2),3),4) =
zero.fn(2).fn(3).fn(4) = (2..4).foldl(fn,zero)
iterateBy
('1 * Int * Int * Int * ('1 * Int -> '1) -> '1)
Left associative folding of specified integers range by step:
iterateBy(zero,2,6,2,fn) = fn(fn(fn(zero,2),4),6) =
zero.fn(2).fn(4).fn(6) = [2,4,6].foldl(fn,zero)
ln
(Float -> Float)
Natural logarithm.
log
(Float -> Float)
Logarithm to the base 10.
log2
(Float -> Float)
Logarithm to the base 2.
pow
(Float * Float -> Float)
Power function:
pow(A,B) = A ** B = A to the power of B
random
(Int -> Int)
Returns a random value in range [0,A-1], or 0 for A<2.
repeatUntil
('1 * ('1 -> '1) * ('1 -> Bool) -> '1)
Repeats the function evaluation until the condition is met:
repeatUntil(x,fn,cond) = if cond(x) then x else
fn(x).repeatUntil(fn,cond)
return
('1 * '2 -> '2)
Evaluates both arguments, discards the 1st result and returns 2nd one.
roundTo
(Float * Float -> Float)
Rounds a float value; 2nd arg. defines a lowest significant digit.
roundTo( 123.456789, 0.01 ) = 123.46
sgn
(Numeric['1] -> Numeric['1])
Returns sign(x) = x/abs(x) or zero if x is zero.
sin
(Float -> Float)
Sine function.
sqrt
(Float -> Float)
Square root.
sysEnvDetails
( -> Record[dirSep:String, maxInt:Int, minInt:Int, module:String,
nlSep:String]) Returns system environment
description.
sysLargestFreeMemoryBlock
( -> Int)
Returns largest free memory block.
sysLastError
( -> String)
Returns the last system error description, or empty string if no errors occurred.
sysLibContent
( -> String)
Prints the Wafl library content in a string.
tan
(Float -> Float)
Tangent function.
~ A
(Int -> Int)
Bitwise NOT operator.
Function / Type and Description
A $
('1 -> String)
Converts a value to a string.
asArray
(Sequence['1]['2] -> Array['2])
Converts a sequence to an array
asBool
(Value['1] -> Bool)
Converts a value to a bool.
asChar
(PrimeNotString['1] -> String)
Converts a value to a character.
asFloat
(PrimeNotFloat['1] -> Float)
Converts a value to a float.
asInt
(PrimeNotInt['1] -> Int)
Converts a value to an integer.
asList
(Sequence['1]['2] -> List['2])
Converts a sequence to a list
asPreview
('1 -> String)
Converts a value to a shortened string.
asString
('1 -> String)
Converts a value to a string.
ascii
(String -> Int)
Returns ascii code of the first character of the string.
ceil
(Float -> Int)
Rounds a float to a closest not smaller integer.
floor
(Float -> Int)
Rounds a float to a closest not greater integer.
round
(Float -> Int)
Rounds a float to a closest integer.
toFmtJSON
('1 -> String)
Converts a value to a string in pretty JSON format.
toJSON
('1 -> String)
Converts a value to a string in JSON format.
toString
(Float * Int -> String)
Converts a float value to a string with given precision.
Function / Type and Description
A ++ B
(SequenceStr['1]['2] * SequenceStr['1]['2] -> SequenceStr['1]['2])
Appends the the second sequence to the first sequence.
A[ : B ]
(SequenceStr['2]['1] * Int -> SequenceStr['2]['1])
Extracts a prefix of the sequence with N elements:
seq[:N]
A[ B : C ]
(SequenceStr['2]['1] * Int * Int -> SequenceStr['2]['1])
Extracts a sequence segment from Nth to (M-1)th element:
seq[N:M]
A[ B : ]
(SequenceStr['2]['1] * Int -> SequenceStr['2]['1])
Extracts a suffix of the sequence with N elements:
seq[N:]
A[ B ]
(Indexable['1]['2]['3] * '2 -> '3)
Extracts an element from the indexable collection:
col[idx]
append
(SequenceStr['1]['2] * SequenceStr['1]['2] -> SequenceStr['1]['2])
Appends the second sequence to the first sequence.
appendAll
(Sequence['1][SequenceStr['2]['3]] -> SequenceStr['2]['3])
Appends all sequences in the given sequence.
contains
(Sequence['2]['1] * '1 -> Bool)
Checks whether the element is contained in the sequence:
seq.contains(el)
count
(Sequence['2]['1] * ('1 -> Bool) -> Int)
Counts the sequence elements that fulfill the condition:
count(seq,cond)
countRep
(Sequence['2]['1] -> Map['1][Int])
Creates a catalog from the sequence. Keys are the distinct elements, and values are the number of occurrences.
cross
(Sequence['4]['1] * Sequence['4]['2] -> Sequence['4][Tuple['1, '2]])
Crosses two sequences to a sequence of pairs.
crossBy
(Sequence['4]['1] * Sequence['4]['2] * ('1 * '2 -> '3)
-> Sequence['4]['3]) Crosses two sequences by given
function: crossBy(seq1, seq2 ,fun)
crossByIdx
(Sequence['4]['1] * Sequence['4]['2] * (Int * '1 * Int * '2 -> '3)
-> Sequence['4]['3]) Crosses two sequences and
indexes by given function: crossByIdx(seq1, seq2 ,fun)
empty
(Indexable['1]['2]['3] -> Bool)
Checks whether the collection is empty.
exists
(Sequence['2]['1] * ('1 -> Bool) -> Bool)
Checks whether any sequence element fulfills the condition:
exists(sequence,cond)
filter
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition
filterMap
(Sequence['3]['1] * ('1 -> Bool) * ('1 -> '2) -> Sequence['3]['2])
Maps a function to sequence elements that meet the condition.
filterMap(seq,cond,fn) == map(filter(seq,cond),fn)
[Deprecated. Use selectMap.]
filterN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2]['1])
Extracts at most N elements that meet the given condition.
[Deprecated. Use selectN.]
filter_seq
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition.
[Deprecated. Use select_seq.]
find
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2][Int])
Finds the indexes of the elements that meet the given condition.
findEq
(Sequence['2]['1] * '1 -> Sequence['2][Int])
Finds the indexes of the elements equal to the given value.
findEqFirst
(Sequence['2]['1] * '1 -> Int)
Finds the index of the first element equal to the given value.
findEqN
(Sequence['2]['1] * '1 * Int -> Sequence['2][Int])
Finds at most N indices of the elements equal to the given value.
findFirst
(Sequence['2]['1] * ('1 -> Bool) -> Int)
Finds the index of the first element that meets the condition.
findN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2][Int])
Finds at most N indices of elements that meet the given condition.
fold
(Sequence['2]['1] * ('1 * '1 -> '1) * '1 -> '1)
Returns the sequence fold by an associative function:
fold == foldl == foldr
fold_seq
(Sequence['2]['1] * ('1 * '1 -> '1) * '1 -> '1)
Non-parallel fold of sequence by an associative function:
fold_seq == foldl == foldr
foldl
(Sequence['3]['2] * ('1 * '2 -> '1) * '1 -> '1)
Returns the left associative fold of sequence elements:
foldl([a,b,c],fn,zero) = fn(fn(fn(zero,a),b),c)
foldr
(Sequence['3]['2] * ('2 * '1 -> '1) * '1 -> '1)
Returns the right associative fold of sequence elements:
foldr([a,b,c],fn,zero) = fn(a,fn(b,fn(c,zero)))
forall
(Sequence['2]['1] * ('1 -> Bool) -> Bool)
Checks whether the given condition applies to all sequence elements:
forall(list,cond).
groupBy
(Sequence['3]['1] * ('1 -> '2) -> Map['2][List['1]])
Creates a catalog from the sequence. Each catalog element is a list of all elements mapped to the key value by the given function.
in
('1 * Sequence['2]['1] -> Bool)
Checks whether the element is in the sequence:
el.in(seq)
leftAggregate
(Sequence['3]['2] * ('1 * '2 -> '1) * '1 -> '1)
Returns the left associative fold of sequence elements.[Deprecated.
Use foldl.]
length
(Indexable['1]['2]['3] -> Int)
Returns the size of the collection.
longerThan
(SequenceStr['1]['2] * Int -> Bool)
Checks whether the sequence is longer than the given integer.
map
(Sequence['2]['1] * ('1 -> '3) -> Sequence['2]['3])
Maps the function to sequence elements: map(seq,fun)
mapFilter
(Sequence['3]['1] * ('1 -> '2) * ('2 -> Bool) -> Sequence['3]['2])
Maps a function to sequence elements and filter the results.
mapFilter(seq,fn,cond) == filter(map(seq,fn),cond)
[Deprecated. Use mapSelect.]
mapIdx
(Sequence['1]['2] * (Int * '2 -> '3) -> Sequence['1]['3])
Maps the function to sequence elements:
mapIdx(seq,fun)
mapIdx_seq
(Sequence['1]['2] * (Int * '2 -> '3) -> Sequence['1]['3])
Maps the function to sequence elements (non-parallel):
mapIdx_seq(seq,fun)
mapSelect
(Sequence['3]['1] * ('1 -> '2) * ('2 -> Bool) -> Sequence['3]['2])
Maps a function to sequence elements and filter the results.
mapSelect(seq,fn,cond) == select(map(seq,fn),cond)
map_seq
(Sequence['2]['1] * ('1 -> '3) -> Sequence['2]['3])
Maps the function to sequence elements (non-parallel):
map_seq(seq,fun)
map_seq_opt
(Array[Int] * (Int -> Int) -> Array[Int])
DO NOT USE Maps the function to array elements (non-parallel):
map_seq(arr,fun)
nonEmpty
(Indexable['1]['2]['3] -> Bool)
Checks whether the collection is non-empty.
rightAggregate
(Sequence['3]['2] * ('2 * '1 -> '1) * '1 -> '1)
Returns the right associative fold of sequence elements.[Deprecated.
Use foldr.]
select
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition
selectByIdx
(Sequence['2]['1] * Sequence['2][Int] -> Sequence['2]['1])
Extracts the sequence elements with given indexes.
selectDistinct
(Sequence['2]['1] -> Sequence['2]['1])
Extracts the locally distinct sequence elements.
selectFrom
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements, from the first one that satisfies the condition, until the end.
selectMap
(Sequence['3]['1] * ('1 -> Bool) * ('1 -> '2) -> Sequence['3]['2])
Maps a function to sequence elements that meet the condition.
selectMap(seq,cond,fn) == map(select(seq,cond),fn)
selectN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2]['1])
Extracts at most N elements that meet the given condition.
selectTrue
(Sequence['2]['1] * Sequence['2][Bool] -> Sequence['2]['1])
Extracts the sequence elements for which the corresponding flag is true.
selectUntil
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements from the first one until the one satisfying the condition.
selectWhile
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements from the first one until the one not satisfying the condition.
select_seq
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that meet the given condition.
size
(Indexable['1]['2]['3] -> Int)
Returns the size of the collection.
sort
(Sequence['2][Value['1]] -> Sequence['2][Value['1]])
Sorts value sequence.
sortAscii
(Sequence['1][String] -> Sequence['1][String])
Sorts string sequence by Ascii table.
sortBy
(Sequence['2]['1] * ('1 * '1 -> Bool) -> Sequence['2]['1])
Sorts the sequence using given comparator.
sortUtf8
(Sequence['1][String] -> Sequence['1][String])
Sorts string sequence by UTF-8 table.
strLen
(String -> Int)
Returns the length of the character string.
sub
(SequenceStr['2]['1] * Int * Int -> SequenceStr['2]['1])
Extracts the subsequence from given 0-based position and given
length: sub(seq,pos,len)
subList
(List['1] * Int * Int -> List['1])
Extracts the sub-list from given 0-based position and given length:
subList(list,pos,len) [Deprecated. Use
sub.]
subStr
(String * Int * Int -> String)
Returns a substring from given position (from 0) and with given
length. [Deprecated. Use sub.]
zip
(Sequence['4]['1] * Sequence['4]['2] -> Sequence['4][Tuple['1, '2]])
Zips two sequences to a sequence of pairs.
zipBy
(Sequence['4]['1] * Sequence['4]['2] * ('1 * '2 -> '3)
-> Sequence['4]['3]) Zips two sequences by given
function: zipBy(seq1, seq2 ,fun)
zipByIdx
(Sequence['4]['1] * Sequence['4]['2] * (Int * '1 * '2 -> '3)
-> Sequence['4]['3]) Zips two sequences and indexes
by given function: zipByIdx(seq1, seq2 ,fun)
zipWith
(Sequence['4]['1] * Sequence['4]['2] * ('1 * '2 -> '3)
-> Sequence['4]['3]) Zips two sequences by given
function: zipWith(seq1, seq2 ,fun) [Deprecated. Use
zipBy.]
Function / Type and Description
% A
(List['1] -> List['1])
Extracts a list tail.
* A
(List['1] -> '1)
Extracts a list head. Not defined for empty list.
A .. B
(Int * Int -> List[Int])
Returns the list of integers in the range:
2..5 = [2,3,4,5]
A : B
('1 * List['1] -> List['1])
Operator that constructs a new list from the given head and tail.
forced
(List['1] -> List['1])
Returns the list, but forces any delayed evaluation.
hd
(List['1] -> '1)
Extracts the list head. Not defined for empty list.
intRange
(Int * Int -> List[Int])
Returns the list of integers in the range:
intRange(2,5) = [2,3,4]
intRangeBy
(Int * Int * Int -> List[Int])
Returns the list of integers in the given range with a given step:
intRangeBy(2,10,2) = [2,4,6,8]
intRangeWithLast
(Int * Int -> List[Int])
Returns the list of integers in the range:
intRangeWithLast(2,5) = [2,3,4,5]
tl
(List['1] -> List['1])
Extracts the list tail.
Function / Type and Description
newArray
(Int * '1 -> Array['1])
Generates an array with given number of elements of given value:
newArray(5,'a') == [# 'a', 'a', 'a', 'a', 'a' #]
newArrayFn
(Int * (Int -> '1) -> Array['1])
Generates an array with given number of elements and computes each of
the elements using given function on its index:
newArrayFn(5,\x:x*x) == [# 0, 1, 4, 9, 16 #]
newArrayFn_seq
(Int * (Int -> '1) -> Array['1])
Generates an array with given number of elements and computes each of
the elements using given function on its index (non-parallel):
newArrayFn_seq(5,\x:x*x) == [# 0, 1, 4, 9, 16 #]
newArrayFn_seq_opt
(Int * (Int -> '1) -> Array['1])
DO NOT USE!!! Generates an array with given number of elements and
computes each of the elements using given function on its index
(non-parallel):
newArrayFn_seq_opt(5,\x:x*x) == [# 0, 1, 4, 9, 16 #]
Function / Type and Description
ifNull
(String * String -> String)
Replaces null with the given value:
ifNull(x,c) = if isNull(x) then c else x
isNull
(String -> Bool)
Checks whether a string is a database NULL value.
strBeg
(String * String -> Bool)
Checks whether the 2nd string is at the beginning of the 1st.
strCat
(String * String -> String)
String concatenation. The same as string addition.
strChars
(String -> List[String])
Splits a string to a list of characters.
strCountSub
(String * String -> Int)
Count occurrences of substring in the given string:
strCountSub('aaaaaA','aa') == 4
strCountSubDis
(String * String -> Int)
Count disjunct occurrences of substring in the given string:
strCountSub('aaaaaA','aa') == 2
strCountSubDisI
(String * String -> Int)
Same as strCountSubDis, but ignores upper and lower case:
strCountSub('aaaaaA','aa') == 3
strCountSubI
(String * String -> Int)
Same as strCountSub, but ignores upper and lower case:
strCountSub('aaaaaA','aa') == 5
strEncodeHtml
(String -> String)
Encodes the string for HTML.
strEncodeSql
(String -> String)
Encodes the string for SQL.
strEncodeUri
(String -> String)
Encodes the string for URI.
strEncodeWafl
(String -> String)
Encodes the string for Wafl code.
strEnd
(String * String -> Bool)
Checks whether the 2nd string is at the end of the 1st.
strHasSub
(String * String -> Bool)
Checks if the string contain the given substring.
strHasSubI
(String * String -> Bool)
Checks if the string contain the given substring, and ignores upper and lower case.
strJoin
(Sequence['1][String] * String -> String)
Joins (concatenates) a sequence of strings, adding the given separator.
strLTrim
(String -> String)
Trims all spaces from left side.
strLastPos
(String * String -> Int)
Finds last position of a substring in the string, or -1 if not found.
strLastPosI
(String * String -> Int)
Same as strLastPos, but ignores upper and lower case.
strLeft
(String * Int -> String)
Returns first N characters of the string. If N is negative, returns all but last -N elements.
strLocale
( -> String)
Returns the current locale name.
strLowerCase
(String -> String)
Converts all letters to lower case.
strNextLastPos
(String * String * Int -> Int)
Finds next last position of a substring in the string, before given pos.
strNextLastPosI
(String * String * Int -> Int)
Same as strNextLastPosI, but ignores upper and lower case.
strNextPos
(String * String * Int -> Int)
Finds next position of a substring in the string, after given pos.
strNextPosI
(String * String * Int -> Int)
Same as strNextPos, but ignores upper and lower case.
strPos
(String * String -> Int)
Finds first position of a substring in the string, or -1 if not found.
strPosI
(String * String -> Int)
Same as strPos, but ignores upper and lower case.
strRTrim
(String -> String)
Trims all spaces from right side.
strReplace
(String * String * String * Int -> String)
Replaces Nth occurrence of substring with given string:
strReplace('ababa','b','c',2) == 'abaca'
strReplaceAll
(String * String * String -> String)
Replaces all occurrences of substring with given string.
strReplaceAllI
(String * String * String -> String)
Same as strReplaceAll, but ignores upper and lower case.
strReplaceI
(String * String * String * Int -> String)
Same as strReplace, but ignores upper and lower case.
strReverse
(String -> String)
Reverses the string.
strRight
(String * Int -> String)
Returns last N characters of the string. If N is negative, returns all but first -N elements.
strSplit
(String * String -> List[String])
Splits a string to a list of string, by extracting the given separator.
strSplitLines
(String -> List[String])
Splits a string to a list of string, by extracting new-line separator.
strSplitLinesTrim
(String -> List[String])
Splits a string to a list of string, by extracting new-line separator. All spaces are trimmed from each segment from left and right side.
strSplitTrim
(String * String -> List[String])
Splits a string to a list of string, by extracting the given separator. All spaces are trimmed from each segment from left and right side.
strTrim
(String -> String)
Trims all spaces from both sides.
strUpperCase
(String -> String)
Converts all letters to upper case.
utfAddBom
(String -> String)
Adds a UTF-8 BOM, if not already present.
utfAt
(String * Int -> String)
Returns a code point at given position, indexed by codepoints.
utfBom
( -> String)
Returns UTF-8 BOM sequence.
utfChars
(String -> List[String])
Splits a string to a list of UTF-8 code points.
utfHasBom
(String -> Bool)
Checks whether a string begins with UTF-8 BOM.
utfIsBom
(String -> Bool)
Checks whether a string content is UTF-8 BOM.
utfIsValid
(String -> Bool)
Checks whether a string is a valid UTF-8 encoded string.
utfLeft
(String * Int -> String)
Returns first N UTF-8 code points of the string.
utfLen
(String -> Int)
Returns UTF-8 length, as a number of complete code points.
utfLessThan
(String * String -> Bool)
String less than operator replacement for UTF-8.
utfRepInvalid
(String * String -> String)
Replaces invalide code points with the given character.
utfReverse
(String -> String)
Reverses UTF-8 string.
utfRight
(String * Int -> String)
Returns last N UTF-8 code points of the string.
utfSlice
(String * Int * Int -> String)
Returns a substring between two given positions, indexing complete UTF-8 code points instead of characters.
utfSub
(String * Int * Int -> String)
Returns a substring from given position (from 0) and with given length, indexing complete UTF-8 code points instead of characters.
utfTrimBom
(String -> String)
Trims leading BOM, if present.
Function / Type and Description
regexMatch
(String * String -> Bool)
Checks whether a string matches given regular expression.
regexMatchI
(String * String -> Bool)
Checks whether a string matches given regular expression. Ignores upper and lower case.
regexPos
(String * String -> Int)
Finds the first regex matching position in the given string.
regexPosAll
(String * String -> List[Int])
Finds all regex matching positions in the given string.
regexPosAllI
(String * String -> List[Int])
Finds all regex matching positions in the given string. Ignores upper and lower case.
regexPosI
(String * String -> Int)
Finds the first regex matching position in the given string. Ignores upper and lower case.
regexReplace
(String * String * String -> String)
Replaces all matching of a regex with the given string.
regexReplaceI
(String * String * String -> String)
Replaces all matching of a regex with the given string. Ignores upper and lower case.
regexSearch
(String * String -> List[String])
Searches for the regex matching in the given string.
regexSearchAll
(String * String -> List[List[String]])
Searches for all regex matchings in the given string.
regexSearchAllI
(String * String -> List[List[String]])
Searches for all regex matchings in the given string. Ignores upper and lower case.
regexSearchI
(String * String -> List[String])
Searches for the regex matching in the given string. Ignores upper and lower case.
Function / Type and Description
dirCreate
(String -> Bool)
Creates a directory.
dirDelete
(String -> Bool)
Deletes a directory, if it is empty
dirExists
(String -> Bool)
Checks whether a directory with the given name exists.
dirFiles
(String -> List[String])
Returns the list of files that fulfill the given filter.
dirSubdirs
(String -> List[String])
Returns the list of subdirs that fulfill the given filter.
fileDelete
(String -> Bool)
Deletes a file.
fileErrors
( -> Bool)
Checks if some errors were detected since last reset.
fileErrorsMsgs
( -> List[String])
Returns the list of all detected file op. error messages.
fileErrorsReset
( -> Bool)
Clears all detected error messages.
fileExists
(String -> Bool)
Checks whether a file with the given name exists.
fileLastError
( -> Bool)
Checks if last file op. produced errors.
fileLastErrorMsg
( -> String)
Returns last file op. error message, or empty string otherwise.
fileNewTempName
( -> String)
Generates a new temporary file name.
fileOrDirExists
(String -> Bool)
Checks whether a file or a directory with the given name exists.
fileRead
(String -> String)
Reads entire file to a string.
fileReadPart
(String * Int * Int -> String)
Reads a part of the file, from given 0-based position and with given
length in bytes: fileReadPart( fname, pos, bytelen )
fileSize
(String -> Int)
Returns the file size.
fileTruncate
(String -> Bool)
Truncates a file. Returns false if truncate fails or file does not exist.
fileWrite
(String * String -> String)
Writes string to a file and truncates old content:
fileWrite( fname, content )
fileWriteAppend
(String * String -> String)
Writes string to a file by appending to the file end.
fileWriteAppend( fname, content )
fileWriteAppendTo
(String * String -> String)
Writes string to a file by appending to the file end.
fileWriteAppendTo( content, fname )
fileWriteTo
(String * String -> String)
Writes string to a file and truncates old content:
fileWriteTo( content, fname )
Function / Type and Description
$ A
(Int -> String)
Return a command line argument:
$1 = cmdLineArgs()[1]
cmdEcho
('1 * String -> '1)
Echo 2nd argument to the console and return the 1st argument.
[Deprecated. Use echoTxt.]
cmdExecute
(String -> String)
Execute the command in the active console. Shows single-line output and supports the input. Returns the entire output as a result.
cmdLastError
( -> Int)
Get integer code of last command status.
cmdLineArgs
( -> List[String])
Return a list of command line arguments.
cmdLineArgsCount
( -> Int)
Return a number of command line arguments.
cmdPrint
(String -> String)
Write a string to the console and return the same string.
[Deprecated. Use echo.]
cmdSetExitCode
('1 * Int -> '1)
Set program exit code to second argument and return the first one.
cmdShellExecute
(String -> String)
Execute the command in the active console. Shows all outputs and supports the input. Returns the entire output as a result.
cmdWafl
(String * List[String] -> String)
Run Wafl program file in a command line shell with given arguments and return the result. All arguments are passed to Wafl program. No arguments apply to interpreter. The output is redirected to the caller’s output stream.
cmdWaflSrc
(String * List[String] -> String)
Run Wafl program source in a command line shell with given arguments and return the result. All arguments are passed to Wafl program. No arguments apply to interpreter. The output is redirected to the caller’s output stream.
cmdWaflX
(String * RecordX[]['1] -> String)
Run Wafl program file in a command line shell with given options and
return the result. Options are specified as a record:
cmdLineArgs - the list of command line arguments to pass to
the program (List[String]); coutFile - the
full filename of the output redirection file; if ‘null’ is
specified, output is discarded; if empty string is specified (default),
the caller’s output stream is used .
conSupportsCtrlCodes
( -> Bool)
Checks whether console output supports control codes.
echo
('1 -> '1)
Write the argument’s string representation to the console and return
the argument: echo(x) == echoTxt( x, x.asString() )
echoCtrl
('1 * String -> '1)
Writes the second argument to the console and return the first argument, but only if the console terminal supports controls codes.
echoFn
('1 * ('1 -> String) -> '1)
Apply the function to the 1st arg. and write the result to the
console: echoFn( x, fn ) == echoTxt( x, fn(x) )
echoLn
('1 -> '1)
Write the argument’s string representation and new line to the
console and return the argument:
echoLn(x) == echoTxt( x, x.asString() + '\n' )
echoOff
('1 -> '1)
Disables console output for the expression. Only the
input function can print the prompt.
echoSilent
('1 -> '1)
Completely disables console output for the expression. Even the
input function cannot print a prompt.
echoTxt
('1 * String -> '1)
Writes the second argument to the console and return the first argument.
echoTxtLn
('1 * String -> '1)
Writes the second argument and new line to the console and return the first argument.
input
(String -> String)
Write a string to the console, wait for input and return it.
Function / Type and Description
Form
( -> Map[String][String])
Get form variable set.
FormValue
(String -> String)
Get value of a given form variable.
Service
( -> Map[String][String])
Get service variable set.
ServiceValue
(String -> String)
Get value of a given service variable.
Session
( -> Map[String][String])
Get session variable set.
SessionValue
(String -> String)
Get value of a given session variable.
answerAction
( -> String)
Automatic action URI generator for forms in questions.
ask
(String -> Map[String][String])
Ask a question by sending the given page content to the client. Returns the next client’s request.
httpGet
(String -> String)
Get WWW content using HTTP/HTTPS GET method. [Deprecated. Use
Curl library.]
httpGetSize
(String -> Int)
Get WWW content length using HTTP/HTTPS HEADER method. [Deprecated.
Use Curl library.]
httpGet_callback
(String * (Int * Int -> Int) -> String)
Get WWW content using HTTP/HTTPS GET method, with progress callback.
[Deprecated. Use Curl library.]
httpHost
( -> String)
Get HTTP host for current request.
httpPathInfo
( -> String)
Get HTTP path info for current request.
httpScript
( -> String)
Get HTTP script for current request.
mimeAsAttachment
(MimeResource * String -> MimeResource)
Adds the MIME resource object filename and flags it as an attachment.
mimeContent
(MimeResource -> String)
Returns the MIME content.
mimeFilename
(MimeResource -> String)
Returns the MIME filename.
mimeResource
(String * String -> MimeResource)
Creates a MIME resource object.
mimeSetFilename
(MimeResource * String -> MimeResource)
Adds the MIME resource object filename.
mimeType
(MimeResource -> String)
Returns the MIME resource type.
Function / Type and Description
filter_par
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that fulfill the given condition.
[Deprecated. Use select_par.]
fold_par
(Sequence['2]['1] * ('1 * '1 -> '1) * '1 -> '1)
Folds the sequence elements in parallel. [‘fn’ must be associative]:
[‘zero’ must be ZERO, because it can be used many times]:
fold_par(seq,fn,zero) == foldl(seq,fn,zero) == foldr(seq,fn,zero)
mapIdx_par
(Sequence['1]['2] * (Int * '2 -> '3) -> Sequence['1]['3])
Maps the fun. to sequence elements (in parallel):
mapIdx_par(seq,fun)
map_par
(Sequence['2]['1] * ('1 -> '3) -> Sequence['2]['3])
Maps the function to sequence elements (in parallel):
map_par(seq,fun)
newArrayFn_par
(Int * (Int -> '1) -> Array['1])
Creates an array with given number of elements and computes each of
the elements using given function on its index (in parallel):
newArrayFn_par(5,\x:x*x) == [# 0, 1, 4, 9, 16 #]
parallel
('1 -> '1)
Marks an expression to be parallelized, if possible.
select_par
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements that fulfill the given condition.
sequential
('1 -> '1)
Force the expression to run sequentially.
This library is currently under development.
Function / Type and Description
Binary libraries are libraries written in C/C++ and distributed with
Wafl interpreter. The Wafl interpreter can list the content of a binary
library using a library name as a listlib option parameter
(or ? option parameter). For more detailed report add the
-verbose option or use ??:
clwafl -listlib:libname
clwafl ? libname
Instead of a library name (e.g. Timer), a filename with
or without extension can be used (libwTimer,
libwTimer.dll or libwTimer.so). Option
-verbose (or the ?? shorthand) can be used to
get a more detailed report:
clwafl -listlib:libname -verbose
clwafl ?? libname
To list all binary libraries, use
clwafl ? libw
For further details please consult the Command Line Reference.
This library is currently under development.
The Curl library is planned to be a basic Wafl interface for libCurl.
Function / Type and Description
httpGet
(String -> String)
Get WWW content using HTTP/HTTPS GET method.
httpGetSize
(String -> Int)
Get WWW content length using HTTP/HTTPS HEADER method.
httpGet_callback
(String * (Int * Int -> Int) -> String)
Get WWW content using HTTP/HTTPS GET method, with progress callback.
This library is currently under development.
The Curl library is planned to be a basic Wafl interface for libCurl.
Function / Type and Description
httpGet
(String -> String)
Get WWW content using HTTP/HTTPS GET method.
httpGetSize
(String -> Int)
Get WWW content length using HTTP/HTTPS HEADER method.
httpGet_callback
(String * (Int * Int -> Int) -> String)
Get WWW content using HTTP/HTTPS GET method, with progress callback.
This library is currently under development.
The Drawing library is a Wafl graphics library based on SDL2.
It is provided as an additional binary Wafl library
libwSDL and must be explicitly included (see section Libraries), as in the following
example:
sdl::newDrawing()
.fillRect(...)
...
where {
sdl = library 'SDL';
}Function / Type and Description
bmpHeight
(SdlBitmap -> Int)
Get bitmap height.
bmpLoadPng
(String -> SdlBitmap)
Load bitmap image from a file.
bmpSub
(SdlBitmap * Int * Int * Int * Int -> SdlBitmap)
Cut out a part of a bitmap image to a new image.
bmpWidth
(SdlBitmap -> Int)
Get bitmap width.
drawBackColor
(SdlDrawing * Int -> SdlDrawing)
Set background color.
drawBmp
(SdlDrawing * SdlBitmap * Int * Int * Int * Int -> SdlDrawing)
Add bitmap image at given rectangle.
drawBmpAt
(SdlDrawing * SdlBitmap * Int * Int -> SdlDrawing)
Add bitmap image at given position.
drawBmpFull
(SdlDrawing * SdlBitmap -> SdlDrawing)
Add bitmap image at viewport.
drawCircle
(SdlDrawing * Int * Int * Int -> SdlDrawing)
Add a circle to the drawing.
drawClear
(SdlDrawing -> SdlDrawing)
Clears the drawing.
drawEllipse
(SdlDrawing * Int * Int * Int * Int -> SdlDrawing)
Add an ellipse to the drawing.
drawFlush
(SdlDrawing -> SdlDrawing)
Flushes the drawing.
drawFlushClear
(SdlDrawing -> SdlDrawing)
Flushes the drawing and returns an empty drawing.
drawLine
(SdlDrawing * Int * Int * Int * Int -> SdlDrawing)
Add a line to the drawing.
drawLineFrom
(SdlDrawing * Int * Int -> SdlDrawing)
Set ‘last point’.
drawLineFromRel
(SdlDrawing * Int * Int -> SdlDrawing)
Set ‘last point’ relative to the ‘last point’.
drawLineOfPixels
(SdlDrawing * Int * Array[Int] -> SdlDrawing)
Draw a row of pixels.
drawLineTo
(SdlDrawing * Int * Int -> SdlDrawing)
Add a line from ‘last point’ to the given point.
drawLineToRel
(SdlDrawing * Int * Int -> SdlDrawing)
Add a line from ‘last point’ to the relative given point.
drawPoint
(SdlDrawing * Int * Int -> SdlDrawing)
Draw a point.
drawPointC
(SdlDrawing * Int * Int * Int -> SdlDrawing)
Draw a point.
drawRect
(SdlDrawing * Int * Int * Int * Int -> SdlDrawing)
Add a rectangle to the drawing.
drawSaveJpeg
(SdlDrawing * String * Int -> SdlDrawing)
Save to JPEG file.
drawSavePng
(SdlDrawing * String -> SdlDrawing)
Save to PNG file.
drawSetColor
(SdlDrawing * Int -> SdlDrawing)
Set line color for following elements.
drawSetLineWidth
(SdlDrawing * Int -> SdlDrawing)
Set line width.
drawSetLogicalSize
(SdlDrawing * Int * Int -> SdlDrawing)
Set logical display size.
fillRect
(SdlDrawing * Int * Int * Int * Int -> SdlDrawing)
Add a filled rectangle to the drawing.
fillRectC
(SdlDrawing * Int * Int * Int * Int * Int -> SdlDrawing)
Add a filled rectangle, with given color, to the drawing.
newDrawing
( -> SdlDrawing)
Create a new empty drawing.
This library is currently under development.
The Timer library is a Wafl library for time measurement. It
is provided as an additional binary Wafl library libwTimer
and must be explicitly included (see section Libraries), as in the following
example:
...
.echoTxt( tlib::start(1)$ + '\n' )
...
.echoTxt( tlib::getPartial(1)$ + '\n' )
...
.echoTxt( tlib::getPartial(1)$ + '\n' )
.echoTxt( tlib::getTotal(1)$ + '\n' )
where {
tlib = library 'Timer';
}Function / Type and Description
getPartial
(Int -> Float)
Get time duration in seconds from last timer activity.
getTotal
(Int -> Float)
Get time duration in seconds from first timer activity.
start
(Int -> Int)
Start or reset a timer with given id.
This library is currently under development.
The Edlib library is a Wafl library for comparison of biological sequences, based on the Edlib C++ library.
It is provided as an additional binary Wafl library
libwEdlib and must be explicitly included (see section Libraries).
Function / Type and Description
alignment
(String * String -> String)
Compute string encoded alignment of two strings.
distance
(String * String -> Int)
Compute edit-distance of two strings.
modifications
(String * String -> String)
Compute string encoded modifications positions.
modificationsLst
(String * String -> String)
Compute string encoded modifications positions list.
The command line interpreter distributes with some libraries. The list of available libraries is a part of the library content report, generated using any of the commands:
clwafl -listlib <filter>
clwafl ? <filter>
clwafl -listlib <filter> -verbose
clwafl ?? <filter>
To list all available Wafl libraries, use
clwafl ? wlib
To generate a library documentation for a Wafl library, use any of the following:
clwafl -doc Console.wlib
clwafl ?? Console.wlib
The format of the library documentation can be selected using the
doc:<format> option, where
<format> can be one of markdown,
text and textWithColors.
clwafl -doc:markdown Console.wlib
If no format is specified, a default format is selected depending on
the context. If the program output is redirected, markdown
is used, if the shell terminal supports ANSI colors,
textWithColors is used, otherwise the text
format is used.
Further details can be found in the Command Line Reference.
Colors.wlib library contains the functions for color
models conversions and handling. Each color model is based on a 32-bit
integer.
This library is currently under development.
Function / Type and Description
asString ( rgb )
(Int -> String)
Convert RGB value to string.
b ( rgb )
(Int -> Int)
Extract blue component of an RGB value.
cssHex ( rgb )
(Int -> String)
Convert RGB value to CSS hex representation.
g ( rgb )
(Int -> Int)
Extract green component of an RGB value.
gray ( rgb )
(Int -> Int)
Convert RGB value to a grayscale value.
h ( rgb )
(Int -> Int)
Convert RGB color value to hue component of HSV model (HSB)
hex ( rgb )
(Int -> String)
Convert RGV value to hex representation.
hsv ( h, s, v )
(Int * PrimeNotFloat['1] * PrimeNotFloat['2] -> Int)
Convert hsv model color value to RGB
r ( rgb )
(Int -> Int)
Extract red component of an RGB value.
rgb ( r, g, b )
(Int * Int * Int -> Int)
Construct a RGB value with givenn components. We assume BE ordering:
rgb = 0x00RRGGBB
rgb_fromCssHex ( hex )
(String -> Int)
Convert a string representation of a hex number to the corresponding
color. The hex string must have prefix #.
rgb_fromHex ( hex )
(String -> Int)
Convert a string representation of a hex number to the corresponding color. The hex string may not have any prefix.
s ( rgb )
(Int -> Int)
Convert RGB color value to saturation component of HSV model (HSB)
v ( rgb )
(Int -> Int)
Convert RGB color value to lightness component of HSV model (HSB)
Console.wlib library contains the ANSI console control
sequences and the functions that generate ANSI console control
sequences.
This library is currently under development.
Definition / Type and Description
bgBlack
String
Set black background color.
bgBlue
String
Set blue background color.
bgCyan
String
Set cyan background color.
bgDefault
String
Set default background color.
bgGreen
String
Set green background color.
bgMagenta
String
Set magenta background color.
bgRed
String
Set red background color.
bgWhite
String
Set white background color.
bgYellow
String
Set yellow background color.
colorReset
String
Reset font color option.
colorResetBlinking
String
Reset blinking color option. [Not working for Windows.]
colorResetBold
String
Reset bold font color option.
colorResetDim
String
Reset dim font color option.
colorResetHidden
String
Reset hidden color option. [Not working for Windows.]
colorResetInverse
String
Reset inverse color option.
colorResetItalic
String
Reset italic color option. [Not working for Windows.]
colorResetStrikeThrough
String
Reset strike font color option. [Not working for Windows.]
colorResetUnderline
String
Reset underline font color option.
colorSetBlinking
String
Set blinking font color option. [Not working for Windows.]
colorSetBold
String
Set bold font color option.
colorSetDim
String
Set dim font color option.
colorSetDoubleUnderline
String
Set double underline font color option. [Not working for Windows.]
colorSetHidden
String
Set hidden font color option. [Not working for Windows.]
colorSetInverse
String
Set inverse font color option.
colorSetItalic
String
Set italic font color option. [Not working for Windows.]
colorSetStrikeThrough
String
Set strike font color option. [Not working for Windows.]
colorSetUnderline
String
Set unerline font color option.
cursorBlinkOff
String
Select the steady cursor behavior.
cursorBlinkOn
String
Select the blink cursor behavior.
cursorRestorePos
String
Restores the saved cursor position.
cursorSavePos
String
Saves the current cursor position.
cursorShapeBarBlinking
String
Select the bar cursor shape.
cursorShapeBarSteady
String
Select the bar steady cursor shape.
cursorShapeBlockBlink
String
Select the block cursor shape.
cursorShapeBlockSteady
String
Select the block steady cursor shape.
cursorShapeDefault
String
Select the default cursor shape.
cursorShapeUnderlineBlink
String
Select the underline cursor shape.
cursorShapeUnderlineSteady
String
Select the underline steady cursor shape.
cursorShowOff
String
Select the hidden cursor.
cursorShowOn
String
Select the visible cursor.
eraseAll
String
Erase the entire display.
eraseFromBegOfDisplay
String
Erase from the beginnning of the display to the cursor.
eraseFromBegOfLine
String
Erase from the beginning of the line to the cursor.
eraseLine
String
Erase the current line.
eraseSavedLines
String
Erase all the lines before the current.
eraseToEndOfDisplay
String
Erase from the cursor to the end of the display.
eraseToEndOfLine
String
Erase from the cursor to the end of the line.
escape
String
Opening escape sequence.
fgBlack
String
Set black text color.
fgBlue
String
Set blue text color.
fgBrightBlue
String
::: ::: {.libtype}
fgBrightGray
String
::: ::: {.libtype}
fgBrightGreen
String
::: ::: {.libtype}
fgBrightRed
String
::: ::: {.libtype}
fgCyan
String
Set cyan text color.
fgDarkBlue
String
::: ::: {.libtype}
fgDarkGreen
String
::: ::: {.libtype}
fgDarkRed
String
::: ::: {.libtype}
fgDefault
String
Set default text color.
fgGray
String
::: ::: {.libtype}
fgGreen
String
Set green text color.
fgLightBlue
String
::: ::: {.libtype}
fgLightGreen
String
::: ::: {.libtype}
fgLightRed
String
::: ::: {.libtype}
fgMagenta
String
Set magenta text color.
fgRed
String
Set red text color.
fgWhite
String
Set white text color.
fgYellow
String
Set yellow text color.
moveHome
String
Move the cursor to home postion.
reset
String
Reset default options.
resetColor
String
Reset defaultonly the color options.
Function / Type and Description
bgSet ( n )
('1 -> String)
Set background color from 256 colors palette.
bgSet24 ( r, g, b )
('1 * '2 * '3 -> String)
Set RGB background color.
charsDelete ( n )
('1 -> String)
Delete n characters from the current position.
charsErase ( n )
('1 -> String)
Replace n characters with blank space.
charsInsert ( n )
('1 -> String)
Insert n blank characters to the current position.
fgSet ( n )
('1 -> String)
Set text color from 256 colors palette.
fgSet24 ( r, g, b )
('1 * '2 * '3 -> String)
Set RGB text color.
linesDelete ( n )
('1 -> String)
Delete n lines.
linesInsert ( n )
('1 -> String)
Insert n blank lines.
moveCol ( col )
('1 -> String)
Move the cursor to the given column.
moveDown ( n )
('1 -> String)
Move the cursor down for n lines.
moveDownBeg ( n )
('1 -> String)
Move the cursor down for n lines and to the line
start.
moveLeft ( n )
('1 -> String)
Move the cursor left for n columns.
moveRight ( n )
('1 -> String)
Move the cursor right for n columns.
moveTo ( line, col )
('1 * '2 -> String)
Move the cursor to the given position.
moveUp ( n )
('1 -> String)
Move the cursor up for n lines.
moveUpBeg ( n )
('1 -> String)
Move the cursor up for n lines and to the line
start.
viewScrollDown ( n )
('1 -> String)
Scroll the window down and move the cursor up.
viewScrollUp ( n )
('1 -> String)
Scroll the window up and move the cursor down.
Csv.wlib library contains the functions for CSV file
processing.
This library is currently under development.
Function / Type and Description
addUtf8Mark ( str )
(String -> String)
Add UTF-8 prefix to the given string. If there is already a UTF-8 prefix, it will not add a second.
build ( lst, rowDelimiter, colDelimiter )
(Sequence['2][Sequence['1][String]] * String * String -> String)
Parse CSV content from a list of rows. Parses the content to a list
of rows, where each row is a list of columns.
Note: Assume no UTF-8 prefix is present.
parse ( body, rowDelimiter, colDelimiter )
(String * String * String -> List[List[String]])
Parse CSV content. Parses the content to a list of rows, where each
row is a list of columns.
Note: Assume no UTF-8
prefix is present.
readFile ( filename, rowDelimiter, colDelimiter )
(String * String * String -> List[List[String]])
Read CSV file. Reads the file and parse it to a list of rows, where each row is a list of columns. If UTF8 file is processed, the UTF prefix is removed from the beginning.
removeUtf8Mark ( str )
(String -> String)
Remove UTF-8 prefix from the given string, if one exist.
utf8mark ()
( -> String)
Get UTF-8 prefix content.
TODO: Not
implemented, yet.
writeFile ( lst, filename, rowDelimiter, colDelimiter )
(Sequence['2][Sequence['1][String]] * String * String * String -> String)
Write CSV file. Takes a list of rows, where each row is a list of
columns, and formats it to CSV using the given delimiters. Finally,
writes the generated document to the given file.
Note: No UTF prefix is added
writeFileUtf8 ( lst, filename, rowDelimiter, colDelimiter )
(Sequence['2][Sequence['1][String]] * String * String * String -> String)
Write CSV file. Takes a list of rows, where each row is a list of columns, and formats it to CSV using the given delimiters. Finally, writes the generated document to the given file, with a UTF-8 prefix added to the beginning.
Paths.wlib library contains some functions for paths
processing.
This library is currently under development.
Function / Type and Description
dirCreateAll ( dir )
(String -> Bool)
Create directory and parent directories, if not exist.
If './a/b/c/d' is given, all four dirs are created,
if already not exist.
pathFileNoExt ( fname )
(String -> String)
Extract a filename from full path without extension. Assume
fname has no ‘/’ at the end. Discard path and keep
extension.
pathFilePath ( fname )
(String -> String)
Extract a folder path from full path. If fname has ‘/’ at the end, the whole string is returned. If no slash is in the string, empty string is returned.
pathFileWithExt ( fname )
(String -> String)
Extract a filename from full path. Assume fname has no
‘/’ at the end. Discard path and keep extension.
Replace.wlib library contains some additional functions
for sequence element replacing. This is a temporary library. Because of
its significance, it will be implemented as a part of the core library.
It is intended to be used as Replace::... so the names have
no replace prefix.
Function / Type and Description
at ( seq, idxs, y )
(Sequence['2]['1] * Sequence['3][Int] * '1 -> Sequence['2]['1])
Replace all elements of a seq at the given positions
idxs by the given value.
atFn ( seq, idxs, fn )
(Sequence['2]['1] * Sequence['3][Int] * ('1 -> '1) -> Sequence['2]['1])
Replace all elements x of a seq at the
given positions idxs by fn(x).
atFnIdx ( seq, idxs, fn )
(Sequence['2]['1] * Sequence['3][Int] * (Int * '1 -> '1) -> Sequence['2]['1])
Replace all elements x of a seq at the
given positions idxs by fn(i,x).
atSeq ( seq, idxs, values )
(Sequence['2]['1] * Sequence['3][Int] * Indexable['4][Int]['1] -> Sequence['2]['1])
Replace all elements of a seq at the given positions
idxs by the given values.
cond ( seq, theCond, y )
(Sequence['2]['1] * ('1 -> Bool) * '1 -> Sequence['2]['1])
Replace all elements x of a seq that
fulfill cond(x) by the value y.
condFn ( seq, cond, fn )
(Sequence['2]['1] * ('1 -> Bool) * ('1 -> '1) -> Sequence['2]['1])
Replace all elements x of a seq that
fulfill cond(x) by the value fn(x).
condFnIdx ( seq, cond, fn )
(Sequence['2]['1] * (Int * '1 -> Bool) * (Int * '1 -> '1) -> Sequence['2]['1])
Replace all elements x of a seq that
fulfill cond(i,x) by the value fn(i,x).
condIdx ( seq, cond, y )
(Sequence['2]['1] * (Int * '1 -> Bool) * '1 -> Sequence['2]['1])
Replace all elements x of a seq that
fulfill cond(i,x) by the value y.
eq ( seq, y, z )
(Sequence['2]['1] * '1 * '1 -> Sequence['2]['1])
Replace all elements of a seq that are equal to
y by the value z.
Test.wlib library contains test functions, used in Wafl
test development.
Definition / Type and Description
reportRunnerResults
(Sequence['4][RecordX[sectionName:String, tests:Sequence['2][RecordX[desc:String, ok:Bool, test:String]['1]]]['3]] -> String)
Basic runner based report generator. If a test program is evaluated by a runner, this function will be used instead of the reportResults.
reportRunnerResultsDetailed
(Sequence['4][RecordX[sectionName:String, tests:Sequence['2][RecordX[desc:String, ok:Bool, test:String]['1]]]['3]] -> String)
Detailed runner based report generator. If a test program is evaluated by a runner, this function will be used instead of the reportResults.
reportRunnerResultsFull
(Sequence['4][RecordX[sectionName:String, tests:Sequence['2][RecordX[desc:String, ok:Bool, test:String]['1]]]['3]] -> String)
Full runner based report generator. If a test program is evaluated by a runner, this function will be used instead of the reportResults.
Function / Type and Description
descSection ( sectionName )
('1 -> '1)
Get the section description.
descTest ( suitename, teststr, test )
('4 * '3 * RecordX[desc:'1, ok:'2]['5] -> Record[desc:'1, ok:'2, test:'3])
Get the test description.
reportResults ( x )
('1 -> '1)
Default test runner report generator. Its purpose is to make possible the direct evaluation of test programs, without the runner support.
test ( arg1 )
('1 -> Record[desc:String, ok:'1])
The basic test function. Test if the argument is
true.
testDiff ( arg1, arg2 )
('1 * '1 -> Record[desc:String, ok:Bool])
Test if the two arguments are different.
testEq ( arg1, arg2 )
('1 * '1 -> Record[desc:String, ok:Bool])
Test if the two arguments are equal.
testEqEps ( arg1, arg2, eps )
(Numeric['1] * Numeric['1] * Numeric['1] -> Record[desc:String, ok:Bool])
Test if the two float arguments are Different at most for
eps.
testFalse ( arg1 )
(Bool -> Record[desc:String, ok:Bool])
Test if the argument is false.
testGE ( arg1, arg2 )
(Value['1] * Value['1] -> Record[desc:String, ok:Bool])
Test if the first argument is greater than or equal to the second argument.
testGT ( arg1, arg2 )
(Value['1] * Value['1] -> Record[desc:String, ok:Bool])
Test if the first argument is greater than the second argument.
testLE ( arg1, arg2 )
(Value['1] * Value['1] -> Record[desc:String, ok:Bool])
Test if the first argument is less than or equal to the second argument.
testLT ( arg1, arg2 )
(Value['1] * Value['1] -> Record[desc:String, ok:Bool])
Test if the first argument is less than the second argument.
TestRunner.wlib library contains functions for running
the Wafl test programs and formatting the results.
Function / Type and Description
analyzeAllTestsInFolder ( dirName, options )
(String * RecordX[reportfile:String]['1] -> List[Tuple[String, String, String, String]])
Analyze all test files in the folder. Returns a listy of tuples:
[ {# fnName, fileName, sectionName, testCode #} ]
analyzeAllTestsInSubfolders ( folder, options )
(String * RecordX[reportfile:String]['1] -> List[Tuple[String, String, String, String]])
Analyze all the tests in the folder and its subfolders. Returns a
list of tuples:
[ {# fnName, fileName, sectionName, testCode #} ]
analyzeSingleTest ( testFileName, options )
(String * RecordX[reportfile:String]['1] -> List[Tuple[String, String, String, String]])
Analyze single test file code. Returns a list of tuples:
[ {# fnName, fileName, sectionName, testCode #} ]
analyzeTestProgramCode ( prgCode, options )
(String * '1 -> List[Tuple[String, String, String]])
Extract test cases data from given program code. Returns a listy of
tuples:
[ {# fnName, sectionName, testCode #} ]
analyzeTestsInFile ( filename, options )
(String * '1 -> List[Tuple[String, String, String, String]])
Returns a listy of tuples:
[ {# fnName, fileName, sectionName, testCode #} ]
analyzeUsageDesc ()
( -> String)
Get the default analyzer usage description.
defaultAnalyzer ()
( -> String)
Default test Analyzer The test analyzer is to be used as a program,
i.e. to write the programs to analyze a collection of the tests. A
typical test analyzer is implemented as follows:
wtr::defaultAnalyzer()
where {
wtr = library file 'testRunner.wlib';
}
For details on the command line arguments,
please read analyzeUsageDesc function definition. The
current version processes only the functions. The operators are not
processed.
defaultRunner ()
( -> String)
Default test runner. The test runner is to be used as a program,
i.e. to write the programs to run a collection of the tests. A typical
test runner is implemented as follows:
wtr::defaultRunner()
where {
wtr = library file 'testRunner.wlib';
}
For details on the command line arguments,
please read usageDesc function definition.
doReportRunnerResults ( testResults, repMode )
(Sequence['4][RecordX[sectionName:String, tests:Sequence['2][RecordX[desc:String, ok:Bool, test:String]['1]]]['3]] * String -> String)
A general test reporter. Generates a result report from the given testing results.
formatAnalyzerReport ( lst, options )
(Sequence['3][TupleX[String, String, String, '1]['2]] * RecordX[detailedreport:Bool, fullreport:Bool]['4] -> String)
Format test analyzer report.
formatAnalyzerReportFull ( lst )
(Sequence['3][TupleX[String, String, String, '1]['2]] -> String)
Format test analyzer report. Full Mode.
formatAnalyzerReportShort ( lst )
(Sequence['2][TupleX[String]['1]] -> String)
Format test analyzer report. Short Mode.
nonTestedFunctionsReport ( lst )
(Sequence['2][TupleX[String]['1]] -> String)
Report on the functions without tests. Does not check operators!
processProgramCode ( prgCode, options )
(String * RecordX[detailedreport:Bool, fullreport:Bool]['1] -> String)
Prepares a test program for evaluation. While any original test programs may run directly, it is required to process it, by replacing some of the elements (section names, report processing functions) to provide for standardized reports, in a more readable form.
programOutputHead ( kind, filename, options )
(String * String * RecordX[reportfile:String]['1] -> String)
::: ::: {.libtype}
reportRunnerResults ( testResults )
(Sequence['4][RecordX[sectionName:String, tests:Sequence['2][RecordX[desc:String, ok:Bool, test:String]['1]]]['3]] -> String)
Test reporter, a basic version. Generates a typical result report from the given testing results.
reportRunnerResultsDetailed ( testResults )
(Sequence['4][RecordX[sectionName:String, tests:Sequence['2][RecordX[desc:String, ok:Bool, test:String]['1]]]['3]] -> String)
Test reporter, a detailed version. Generates a detail result report from the given testing results.
reportRunnerResultsFull ( testResults )
(Sequence['4][RecordX[sectionName:String, tests:Sequence['2][RecordX[desc:String, ok:Bool, test:String]['1]]]['3]] -> String)
Test reporter, a full version. Generates a full result report from the given testing results.
runAllTestsInSubfolders ( folder, options )
(String * RecordX[detailedreport:Bool, fullreport:Bool, reportfile:String]['1] -> String)
Run all test Wafl programs in the given folder and all of its direct subfolders, using the given options. No program will run in the deeper subfolders. It is expected that all the programs use the same test library to check the conditions and report results. The report is saved to the given report file.
runSingleTest ( testFileName, options )
(String * RecordX[detailedreport:Bool, fullreport:Bool, reportfile:String]['1] -> String)
Run single test program, using the given options.
theAnalyzer ( options )
(RecordX[detailedreport:Bool, directory:String, file:String, fullreport:Bool, help:Bool, reportfile:String]['1] -> String)
Test analyzer. See defaultAnalyzer() for
details.
theRunner ( options )
(RecordX[detailedreport:Bool, directory:String, file:String, fullreport:Bool, help:Bool, lastreport:Bool, reportfile:String]['1] -> String)
Test runner. See defaultRunner() for
details.
usageDesc ()
( -> String)
Get the default runner usage description.
writeReportToFile ( report )
(String -> String)
Write the report to the file specified with argument
-repFile:<filename>. This function evaluates as a
part of the tests, and thus requires explicit argument checking.
Here we present the syntax of the Wafl command line interpreter. This document is generated automatically. To get the current command line options, please run:
clwafl -help
The name of the Command line interpreter program may differ,
depending on the installation package. It can be clWafl,
clwafl or wafl.
to run a program:
clwafl2r [<options>] <program-file-name> [<arguments>]
to run inline source code:
clwafl2r [<options>] -code "<source-code>" [-args <arguments>]
to apply some tools:
clwafl2r [<options>] [<program-or-library-file-name>]
where <options> is a space separated list that can contain
the following options:
Program arguments and environment:
-code "<source-code>"
Specify the Wafl program code inline, as a part of the
command line. Quotes are strongly recommended.
-args <args> Specify Wafl program command line arguments.
Allowed only if inline source code is used.
-dir:<app-directory>
Specify the application directory.
Default is the path of the program file, or '.'.
-workdir:<working-directory>
Specify the working directory.
Default is program file path, or '.'.
-libdir:<lib-directory>
Specify a library search directory.
-lib:<name>:<file>
Load Wafl library <file> as parameterized library <name>.
-env:<env-var-name>=<env-var-value>
Define an environment variable.
Database access:
-db:<database-driver>:<database-alias>
Specify the database driver and database alias.
-db:<database-alias>
Specify the database alias.
-dbdriver:<database-driver>
Specify the database driver.
-user:<database-connection-username>:<password>
Specify the database user and password.
-user:<database-connection-username>
Specify the database user.
-pwd:<database-connection-password>
Specify the database user's password.
Runtime options:
-repeat:<n> Run the program <n> times.
-memory Print the memory report.
-timer Measure execution time.
-title:<a-title>
Set the title of the console window.
-nocolors Disable using terminal ANSI color codes.
-wait Wait for a key after execution.
Optimization and evaluation mode options:
-stack-std, -stdstack
Use standard stack.
-stack-ext, -extstack
Use extendable stack (default).
-stack-size:<n>
Initial stack size in KiB [0-1000000].
For extendable stack, the size of the first segment.
For standard stack, this is the stack size.
The default is 0, which means OS defined.
-stack-blocksize:<n>
Single extendable stack block size in KiB [250-100000].
Only applies to extendable stack. The default is 1024KiB.
-stack-limit:<n>
Total stack space limit for all threads together in MiB.
Only applies to extendable stack. Range is [0-100000].
The default is 0, which means unlimited.
-stack-threadlimit:<n>
Maximum permitted stack size in MiB [0-100000].
Only applies to extendable stack.
The default is 0, which means unlimited.
-jit JIT optimization [experimental].
-jit-rebuild JIT optimization with module rebuild [experimental].
-jit-ast JIT optimization on AST [experimental].
-jit-aeg JIT optimization on AEG [experimental].
-dis-tailcalls
Disable tail calls optimization.
Parallelization options:
-pd, -parallel-disable
Disable implicit parallelization (default).
-pa, -parallel-auto
Enable implicit parallelization.
-ps, -parallel
Suggest the implicit parallelization. Like auto,
but biased more towards parallelization.
-pf, -parallel-force
Use parallelization wherever possible.
Debugging and testing options:
-debug Use the debug mode.
-silent-echo Disable echo output.
-silent Disable all console output.
-options Print all selected options.
-msgs Print the compilation messages.
-segvcatch Use `segvcatch` library to catch system signals.
-nornd Run without initialization of random number generator.
The same random number sequence is generated each time,
except when parallel functions are used.
-short Print only a short preview of the program result.
Tools:
-check Check whether a program or library is correct.
-checkdir Check whether all programs in the current directory
are correct.
-checkdir:<dir>
Check whether all programs in <dir> are correct.
-checkapp Check whether all programs in the current directory
and its subdirectories are correct.
-checkapp:<dir>
Check whether all programs in the directory <dir>
and its subdirectories are correct.
-parserast Print the abstract syntax tree generated by parser.
-parsersrc Print the source code generated from the AST
-builderaeg Print the abs. evaluation graph generated by the builder.
-buildersrc Print the 'meta' source code generated from the AEG.
-doc:<format> Generate documentation based on the program comments.
Works for programs and Wafl libraries.
Formats include `markdown`, `text` and `text-colors`.
-doc Generate documentation. Default format is `text-colors`,
but if output is redirected, default is `markdown`.
-create-ini-file
Generates `wafl.ini` file in the working directory.
-listlib List the system library contents.
Binary libraries are excluded.
-listlib:<w> List the system library elements with names containing <w>,
or the content of a binary library with filename <w>.
? <w> Same as `-listlib:<w>`.
?? <w> Same as `-verbose -listlib:<w>`.
Additional options:
-help Print this usage description.
-version Print version description.
-verbose Print more details on `-listlib`, `-version` and `-memory`.
Configuration files are defined at three basic levels:
Global configuration defines options that apply
to all users and all Wafl programs, until overridden. Global
configuration is specified in the file
C:\ProgramData\wafl.ini for Windows, or
/etc/wafl.ini for Linux.
User configuration defines options that are
specific to a user. These can override global options and apply to all
programs run by the user, unless overridden by local configuration. User
configuration is specified in the file
%USERPROFILE%\.wafl.ini (usually
C:\Users\<username>\.wafl.ini) for Windows or
${HOME}/.wafl.ini (usually
/home/<username>/.wafl.ini) for Linux.
Local configuration defines options that are
specific to an application. They can override global and user
configurations. Local configuration is specified in the
wafl.ini file in the working directory. It is used only for
command line programs.
When a Wafl program is run from the command line, configuration files are processed (global, user and local) and used to set the default values of the program’s options. If an option is explicitly specified using the command-line options, then it overrides the default values set in the configuration files.
Similarly, for non-command-line Wafl programs (like Web service, Jupyter and others) the service configuration file can override some of the more generally defined configuration parameters. For such programs, local configuration files are not used.
Wafl configuration files use the usual ini-file syntax.
[section-name-1]
par-name-1 = value-1
par-name-2 = value-2
# comment
[section-name-2]
par-name-3 = value-1
par-name-4 = value-2
# comment
...The following sections are defined:
command-line - defines the parameters that correspond
to command line options, and represent their default values;jit-g++ - defines the parameters for JIT compilation
and linking using g++ tools;jit-msvc - defines the parameters for JIT compilation
and linking using MS Visual Studio.database - defines some default database
parameters.Other sections can be defined in future.
command-lineFor each command line option -option and parameter
-param:value there is a corresponding parameter in the
command-line section:
-option to be set by default,
specify the parameter option = 1.-option to be reset by default,
specify the parameter option = 0.option-mode = name (see parallel or
stack).param = value.-NN to
its name. Such parameters are used in a lexical order.Here is an example configuration for command-line
section, with all applicable parameters:
[command-line]
# This section defines the default command line program options.
# For each parameter, the corresponding command line option
# is specified in the comment.
# DB connection username and password
# must not be specified in configuration file!
# Set environmet variables
# -env:<name>=<value>
env-01 = DATA_PATH = /data
env-02 = MEDIA_PATH = /data/media
# Default system library directory
# Cannot be overriden by command line options!
# The default is `${WAFL_PATH}/lib`, but if another version
# of the system library is used (e.g. during the development)
# then it can be specified here.
libdir-system = /dev/Wafl/lib
# Default application library directory
# -libdir:<value>
libdir = ~/mywafllibs
# Load a parametrized library by default
# -lib:<name>:<file>
lib-01 = libOs = myoslib
lib-02 = libDb = mydblib
# Set parallelization mode [auto, disable, suggest, force]
# -pa, -pd, -ps, -pf
parallel-mode = auto
# Use extendable stack [extendable, standard]
# -stack-ext, -stack-std
stack-mode = extendable
# Initial steck size in MiB
# -stack-size:<value>
stack-size = 10
# Size of individual stack blocks in KiB
# -stack-blocksize:<value>
stack-blocksize = 1000
# Total stack limit for all threads together in GiB
# -stack-limit:<value>
stack-limit = 0
# Stack limit for individual threads in MiB
# -stack-threadlimit:<value>
stack-threadlimit = 1000
# Use JIT
# -jit
jit = 0
# Default JIT mode [aeg, ast]
# NOTE: This option does not turn JIT on!
# -jit-aeg, -jit-ast
jit-mode = aeg
# Set JIT rebuild by default
# NOTE: This option does not turn JIT on!
# -jit-rebuild
jit-rebuild = 0
# The other aplicable parameters
# Please see command line options for details
title = My Wafl command line window title
timer = 0
wait = 0
debug = 0
segvcatch = 0
dis-tailcalls = 0
nornd = 0jit-g++Here is a complete example, for Ubuntu Linux 24.10, g++ and appropriate Wafl Core:
[jit-g++]
include-path-1 = /home/smalkov/dev/WaflLocal/bld/debug/_install/waflcore/GNU_12.4.0_uxmkfl_x86_64_8_1_CPP20_Debug/include
compiler-options-1 = -std=c++20 -fpic -ldl -shared
compiler-options-release = -O3 -DNDEBUG
link-options-1 = ~/dev/WaflLocal/bld/debug/_install/waflcore/GNU_12.4.0_uxmkfl_x86_64_8_1_CPP20_Debug/lib/libWaflCore.a;
#keep-source = 1Please see JIT documentation for details.
jit-msvcHere is a complete example, for VS 2022, Windows 10 and appropriate Wafl Core:
[jit-msvc]
tools-path = C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.42.34433\bin\Hostx64\x64
include-path-1 = C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.42.34433\include
include-path-2 = C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\ucrt
include-path-3 = E:\Wafl\bld\vs17_release\_install\waflcore\MSVC_143_win64_AMD64_8_1_CPP20_Release\include
lib-path-1 = C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.42.34433\lib\x64
lib-path-2 = C:\Program Files (x86)\Windows Kits\10\lib\10.0.22621.0\ucrt\x64
lib-path-3 = C:\Program Files (x86)\Windows Kits\10\lib\10.0.22621.0\um\x64
lib-path-4 = E:\Wafl\bld\vs17_release\_install\waflcore\MSVC_143_win64_AMD64_8_1_CPP20_Release\lib
compiler-options-1 = /nologo /std:c++20 /EHsc /Gd /MP
compiler-options-2 = /Fo: "%TEMP%"
compiler-options-3 = /D _WINDLL /D _ITERATOR_DEBUG_LEVEL=0 /D WIN32 /D _WINDOWS /D _WIN32_WINNT=0x0A00
# compiler-options-debug = /MTd /Od /Ob0
compiler-options-release = /MT /O2
link-options-1 = /NOLOGO /DLL /DYNAMICBASE /MACHINE:X64 /LTCG /NOIMPLIB /NOEXP
link-options-2 = libWaflCore.lib
link-options-3 = kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib
#keep-source = 1databaseHere is an example:
[database]
# Default database driver, can be overridden by: -dbdriver:<value>
default-driver = Db2v11
# Default database, can be overridden by: -db:<value>
default-db = Test
default-query-type = untypedPlease see Configuration File Database Parameters for details.
Wafl supports ANSI console control codes, often referred to as ANSI escape sequences. The control sequences are printed to the console like ordinary text, but allow control of color, font, cursor position and other options.
The Wafl library Console.wlib contains many predefined
sequences. It can be used to write more readable Wafl code. For example,
the following two code segments generate and output the same result:
"Regular\e[34mBlue\e[0mRegular""Regular"
+ con::colorFgBlue + "Blue" + con::resetColor
+ "Regular"
where {
con = library file 'Console.wlib';
}Some of the material has been collected from the Wafl Project source files. These files document some features that have not yet been prepared in regular tutorial form. Please forgive us for some technical inconsistencies in these files.
Wafl Evaluator provides a basic support for Just In Time compilation. Wafl JIT optimization is based on using the extern binary libraries. The interpreter first analyzes the code and selects the functions that can translate to C++. Then it generates the C++ source file and builds the binary library. The built library is dynamically loaded and linked. The linking process effectively replaces the usual Wafl evaluator nodes with dynamically loaded library nodes.
JIT optimization can be used it two different phases and with some significant differences in the algorithm and the capabilities: on abstract syntax tree (AST) and on abstract evaluation graph (AEG). It cannot be used in both ways. The default behavior is to have the JIT optimization on the AEG.
To use JIT optimization, the user/programmer needs to:
JIT optimization can be used it two different phases and with some significant differences in the algorithm and the capabilities: on abstract syntax tree (AST) and on abstract evaluation graph (AEG). It cannot be used in both ways. The default behavior is to have the JIT optimization on the AEG.
To enable JIT one of the JIT command line options has to be specified. By default the JIT optimization is disabled.
-jit-ast - Enable JIT optimization on
AST;-jit-aeg - Enable JIT optimization on
AEG;-jit - Enable a default JIT optimization (AEG
in this version);-jit-rebuild - Enable a default JIT
optimization and force a module rebuild.The library building process is based on the extern libraries and
Wafl Core libraries. To have all the tools available, it is
required to have the development environment set properly, or to provide
the wafl.ini configuration file in the working directory.
Configuration file uses the traditional ini file format, with
sections, names and values. The line comments begin with #
or //.
For Linux operating system it is assumed that g++ tools are used. JIT
configuration options are located in [jit-g++] section. It
is usual to have all the general options already set in the command
shell environment, so for Linux it is usually enough to se just the
Wafl-specific options, like include path and some compiler and linker
options.
All the paths are specified without quotes.
Include Search Path contains a list of directories where C++ header
files are located. It must include all compiler libraries, OS system
libraries and Wafl Core library, but the environment is usually already
prepared in general. The include files search path is specified by using
multiple lines with variables named like include-path-...,
or by using a single variable include-path with directories
separated by ;. For example:
include-path-1 = ~/dev/WaflLocal/bld/debug/_install/waflcore/GNU_12.4.0_uxmkfl_x86_64_8_1_CPP20_Debug/includeCompiler options are specified by using multiple lines with variables
named like compiler-options-..., or by using a single
variable compiler-options. Options are separated by blank
spaces. There are no different configurations for debug and release
builds. The same built JIT modules can be used by both debug and release
mode Wafl interpreters.
For example:
compiler-options-1 = -std=c++20 -fpic -ldl -shared
compiler-options-release = -O3 -DNDEBUGLinker options are specified by using multiple lines with variables
named like link-options-..., or by using a single variable
link-options. Options are separated by blank spaces. There
are no different configurations for debug and release builds. The same
built JIT modules can be used by both debug and release mode Wafl
interpreters.
Link options should include all the libraries required to build the JIT module, including the Wafl Core library.
For example:
link-options-1 = ~/dev/WaflLocal/bld/debug/_install/waflcore/GNU_12.4.0_uxmkfl_x86_64_8_1_CPP20_Debug/lib/libWaflCore.a;When JIT build process is completed, the generated C++ source files
and command scripts are deleted. To keep the sources in user’s
TEMP directory, use configuration option
keep-source=1.
Here is a complete example, for Ubuntu Linux 24.10, g++ and appropriate Wafl Core:
[jit-g++]
include-path-1 = /home/smalkov/dev/WaflLocal/bld/debug/_install/waflcore/GNU_12.4.0_uxmkfl_x86_64_8_1_CPP20_Debug/include
compiler-options-1 = -std=c++20 -fpic -ldl -shared
compiler-options-release = -O3 -DNDEBUG
link-options-1 = ~/dev/WaflLocal/bld/debug/_install/waflcore/GNU_12.4.0_uxmkfl_x86_64_8_1_CPP20_Debug/lib/libWaflCore.a;For Windows operating system it is assumed the MSVC tools are used.
JIT configuration options are located in [jit-msvc]
section. The options include tools path, include path, library path,
compiler options and linker options.
All the paths are specified without quotes.
Tools Search Path is a list of directories where compiler and linker
are located. It is specified as a value of tools-path
parameter. It is usually a single directory, but it can be a list of
directories separated by ;. For example:
tools-path = C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.42.34433\bin\Hostx64\x64Include Search Path contains a list of directories where C++ header
files are located. It must include all compiler libraries, OS system
libraries and Wafl Core library. The include files search path is
specified by using multiple lines with variables named like
include-path-..., or by using a single variable
include-path with directories separated by ;.
For example:
include-path-1 = C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.42.34433\include
include-path-2 = C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\ucrt
include-path-3 = E:\Wafl\bld\vs17_release\_install\waflcore\MSVC_143_win64_AMD64_8_1_CPP20_Release\includeLibrary Search Path contains a list of directories where C++
libraries are located. It must include all the libraries used to build
the JIT extern library, except the ones that are specified with their
full absolute path in linker options. The library files search path is
specified by using multiple lines with variables named like
lib-path-..., or by using a single variable
lib-path with directories separated by ;. For
example:
lib-path-1 = C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.42.34433\lib\x64
lib-path-2 = C:\Program Files (x86)\Windows Kits\10\lib\10.0.22621.0\ucrt\x64
lib-path-3 = C:\Program Files (x86)\Windows Kits\10\lib\10.0.22621.0\um\x64
lib-path-4 = E:\Wafl\bld\vs17_release\_install\waflcore\MSVC_143_win64_AMD64_8_1_CPP20_Release\libCompiler options are specified by using multiple lines with variables
named like compiler-options-..., or by using a single
variable compiler-options. Options are separated by blank
spaces. There are no different configurations for debug and release
builds. The same built JIT modules can be used by both debug and release
mode Wafl interpreters.
For example:
compiler-options-1 = /nologo /std:c++20 /EHsc /Gd /MP
compiler-options-2 = /Fo: "%TEMP%"
compiler-options-3 = /D _WINDLL /D _ITERATOR_DEBUG_LEVEL=0 /D WIN32 /D _WINDOWS /D _WIN32_WINNT=0x0A00
# compiler-options-debug = /MTd /Od /Ob0
compiler-options-release = /MT /O2Linker options are specified by using multiple lines with variables
named like link-options-..., or by using a single variable
link-options. Options are separated by blank spaces. There
are no different configurations for debug and release builds. The same
built JIT modules can be used by both debug and release mode Wafl
interpreters.
Link options should include all the libraries required to build the JIT module.
For example:
link-options-1 = /NOLOGO /DLL /DYNAMICBASE /MACHINE:X64 /LTCG /NOIMPLIB /NOEXP
link-options-2 = libWaflCore.lib
link-options-3 = kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.libWhen JIT build process is completed, the generated C++ source files
and command scripts are deleted. To keep the sources in user’s
TEMP directory, use configuration option
keep-source=1.
Here is a complete example, for VS 2022, Windows 10 and appropriate Wafl Core:
[jit-msvc]
tools-path = C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.42.34433\bin\Hostx64\x64
include-path-1 = C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.42.34433\include
include-path-2 = C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\ucrt
include-path-3 = E:\Wafl\bld\vs17_release\_install\waflcore\MSVC_143_win64_AMD64_8_1_CPP20_Release\include
lib-path-1 = C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.42.34433\lib\x64
lib-path-2 = C:\Program Files (x86)\Windows Kits\10\lib\10.0.22621.0\ucrt\x64
lib-path-3 = C:\Program Files (x86)\Windows Kits\10\lib\10.0.22621.0\um\x64
lib-path-4 = E:\Wafl\bld\vs17_release\_install\waflcore\MSVC_143_win64_AMD64_8_1_CPP20_Release\lib
compiler-options-1 = /nologo /std:c++20 /EHsc /Gd /MP
compiler-options-2 = /Fo: "%TEMP%"
compiler-options-3 = /D _WINDLL /D _ITERATOR_DEBUG_LEVEL=0 /D WIN32 /D _WINDOWS /D _WIN32_WINNT=0x0A00
# compiler-options-debug = /MTd /Od /Ob0
compiler-options-release = /MT /O2
link-options-1 = /NOLOGO /DLL /DYNAMICBASE /MACHINE:X64 /LTCG /NOIMPLIB /NOEXP
link-options-2 = libWaflCore.lib
link-options-3 = kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.libWafl Binary Libraries are function libraries
developed in C++. The main support for implementation of Wafl Binary
Libraries is the libExtern library.
This document describes the libExtern library and the
Wafl Binary Library development process. libExtern is pure
header library. It consists of two header files:
ExternLibInterface.h, which defines a function
arguments marshalling interface, andExternLibBase.h, which defines the library catalog
interface and the catalog object itself.Each C++ source file implementing a binary library should include the first header, but only one source file should and must include the second.
The library loading is implemented in
BinaryLibraryDefinition class and method
ImportLibrary.
Based on the libExtern library, the
WaflCore project is exported as waflcore
library. It includes all elements of the Wafl project that are required
to build binary libraries out of the main Wafl project.
The usage of the waflcore exported library is similar to
the usage of the libExtern library. It consists of:
include/waflcore/libExtern.h is a header file,
equivalent to ExternLibInterface.h. Ito should be included
by each module that implements some parts of the binary library
interface;include/waflcore/libExternBase.h is a header file
equivalent to ExternLibBase.h. It must be included by one
and only one module, because it defines some interface objects;lib/libWaflCore.lib or lib/libWaflCore.a
(depending on the platform) is a static library that contains all
required elements of the Wafl project.Binary defined libraries are used in a similar way like the common
Wafl defined libraries. To explicitly declare a binary function, the
extern prefix is used:
myLib = library extern '<library file name>';The library filename is usually specified without any extension. Each
implementation will automatically add the appropriate extension
(.dll for Windows and .so for Linux) when
loading the library. Moreover, if the library is not found, the
interpreter will try to add a prefix libw to the library
name. Also, if prefix extern is not specified, the library
manager will first try to load a binary library, and if not found, then
it will try to load a regular Wafl library.
For example, the library defined in a file libwRegex.so
can be declared as:
regex = library 'Regex';The library functions are accessed as usual, using ::
domain operator:
... myLib::fn1 ...
... myLib::fn2 ...To list the content of a binary library, the usual command line interface can be used. Because a large number of extern libraries may exist, the extern library content is listed only if a library name is used as a filter.
For example, to list the contents of a binary library
libwRegex.so, one of the following commands can be
used:
clwafl ? Regex
clwafl ? libwRegex
clwafl ? libwRegex.soFor more detailed descriptions, use ??:
clwafl ?? RegexThe import syntax is also available.
Each exported binary function have to have a so called canonical form:
void aFunction( EvCell* reserved, const EvCell* args )The first argument reserved is pointer to the
preallocated (non-constructed) space where a result will be stored. The
second argument args points to an array of function
arguments. All arguments and result are objects of EvCell
type.
It is, of course, usual to define (or to already have) a function in a regular way, like:
int fn( int, float, bool )or like methods in some of the defined classes, like:
int Type::fn( ... )The corresponding canonical function can be defined explicitly or automatically. If the canonical function is expected to be built automatically, then some of the following sections are not important. However, if the canonical function is defined explicitly, then it must follow some rules. The rules are explained in the following sections.
This section is not important if canonical functions are built automatically.
Each canonical function implementation has to do three jobs:
EvCell objects;EvCell object.The conversion of arguments and result is covered in the following
sections. It is not too complex, but for the most of the cases it
follows the same rules. This is why a wrapper function
wrappedFn is provided (as well as a wrapper class
FunctionWrapper), to make the wrapping of a
regular function easier.
For example, the following expression defines a canonical version of
library function for a given regular function fn:
wrappedFn< decltype(fn), fn >The wrapper function has the appropriate canonical type. It marshalls
all the arguments to the regular function, invokes the function and
finally marshalls the result back from a regular form to the
EvCell form.
The wrapper function (and the wrapper class) works for functions with
argument types supported by getArg template function and
result type supported by setResult template function. For
other types these template functions have to be specialized, first. See
the following sections for details.
This section is not important if canonical functions are built automatically.
Please consult the section on the supported types.
Each argument is read from the appropriate EvCell object
and marshalled to the appropriate expected type. It can be done manually
or using function template:
T getArg<T>( const EvCell* arg )
The template is defined for the common types as follows:
T (including bool):
(T) arg->AsInteger()int:
(int) arg->AsInteger()T:
(T) arg->AsFloat()double:
(double) arg->AsFloat()char* and const char*:
arg->AsString().c_str()std::string and std::string&:
arg->AsString().asBaseString()sml::String and sml::String&:
arg->AsString()T*, where T is a library defined
class, which is exported from the library:
(T*) TheLibrary.ParentContext() .GetObjectFromExternalObjectCell( arg )T, where T is an internal library
defined class, which is not exported from the library, the marshalling
uses Wafl records.To support another type, it is required to define the appropriate
specialization of the template function getArg. For
example, to support the type A, a specialization like this
is required:
template<>
A getArg<A>( const EvCell* arg )
{
...
return (A) ...;
}The new specializations will be used by the wrappers, too.
This section is not important if canonical functions are built automatically.
Function result has to be converted to Wafl EvCell
objects. It can be done manually or using function template:
void setResult<T>( EvCell* reserved, T value )The setResult is defined for the common types based on
the CellFactory functions (and wrappers), which are
available from the callers context. The supported types include integral
types, bool, floating point types, string types
(char*, std::string, sml::String)
and object pointers.
For example, integer results are marshalled like this:
TheLibrary.ParentContext()
.CreateConstIntegerCellAt( reserved, (fInteger) value );To support another type, it is required to define the appropriate
specialization of the template function setResult. For
example, to support a type A, a specialization like this is
required:
template<>
void setResult<A>( EvCell* reserved, A value )
{
... create a cell at reserved ...
... with given value of type A ...
}The new specializations will be used by the wrappers, too.
This section is not important if canonical functions are built automatically.
When registering a function, its Wafl type is specified using a type
string representation. For example, a function that maps an integer to a
float has a type ( Int -> Float ). For more details on
the types, see Wafl documents.
A function template is provided to automatically generate the type strings:
const fString& waflFnTypeName<FN>()It creates a function type string using another template function to get individual argument or result type name:
const char* waflSimpleTypeName<T>()Template function waflSimpleTypeName supports the common
types (just like getArg and setResult) and
requires further specializations for advanced cases.
All primitive types are supported:
Int type size corresponds to build settings and
ISA. For 64-bit processors it is signed 64 bit integer. However, any
integer type is allowed, but take care of the conversions;Float type is defined as C++ double
type. However, any float type may be used, but take care of the
conversions;Bool type corresponds to C++ bool
type.NOTE: If the canonical versions of the functions and/or the function types are automatically created, then the following additional constraints hold:
fBool is used as a logical type, almost equivalent
to the bool type. However, this type is defined as a 64-bit
unsigned integer. If integers are used, please take care not to use
64-bit unsigned integers (including size_t and similar
types) for arguments and results, because they may be miss-detected as a
logical type.
fInteger type represents 64-bit signed
integer.Wafl String type is internally defined as
fString, which is a synonym for sml::String.
They are both defined in the specified header files. Of course,
std::string is supported, also.
Because of the different handling of memory allocation in dynamic
modules in some environments (in Windows, for example, each DLL may have
its own memory heap), the String arguments and results have
to be freed in the same module where they are created.
String as argumentSo, the String arguments must not be freed in binary
function. Moreover, they must not be copied. Please copy only the binary
char* representation. This is why String&
is better to be used than simple String.
TO DO! The solution is modified and the following sentence is false. Check and improve!
However, if there is a specific case when a String
argument has to be destructed, then first the
void FreeString() const method must be invoked on the
string.
String as resultUsing String as result is similar to the argument case.
The string itself should be copied. This is why it is better to use
char* as result than String.
The following array types are supported:
Array[Int] corresponds to C++ type
fArrayInt;Array[Float] corresponds to C++ type
fArrayFloat;Array[Bool] corresponds to C++ type
fArrayBool;Array[String] corresponds to C++ type
fArrayString.Array[Array[Int]] corresponds to C++ type
fArrayInt2;Array[Array[Array[Int]]] corresponds to C++
type fArrayInt3;Float, Bool and
String.Types fArrayInt, fArrayFloat,
fArrayBool and fArrayString are interfaces to
Wafl arrays.
Arguments of array types must be specified as const
pointers, like:
size_t getArrayLen( const fArrayInt* arr )Such arguments must not be deleted by library functions.
Array must be returned by pointer. It will be deleted by Wafl evaluator after usage.
fArrayInt* getRandomArray( int range, int size )See examples for more details.
Array[...] corresponds to C++ type
fArrayOfCells. It is not recognized automatically and has
to be specified manually.Records are mapped to C++ classes defined in the library. Such classes are not exported from the library.
As C++ has no reflection features, it is not possible to implement
complete marshalling of record/class types automatically. The developers
have to provide explicitly the mapping of the types. The mapping is
specified by implementing a specialization of the template
RecordMarshalling.
To map a class MyClass to a Wafl record type, there are
a few requirements to fulfill:
template<> class RecordMarshalling<MyClass>
with four static methods has to be implementedMyClass WaflToCpp( const WAFL_ExternLib::Record* record )
for conversion of arguments of a Wafl record type to objects of the
MyClass class.
MyClass.MyClass should support a move
construction. static MyClass WaflToCpp( const WAFL_ExternLib::Record* record )
{
MyClass obj {
getArg_safe<int>( record->ElementByName( "intAttr1" )),
getArg_safe<int>( record->ElementByName( "intAttr2" ))
};
return obj;
}void CppToWafl( WAFL_ExternLib::Record* record, const MyClass& obj )
for conversion of function results of the MyClass type to a
Wafl record type.
record and with the appropriate attribute names.MyClass argument is
destroyed. static void CppToWafl( WAFL_ExternLib::Record* record, const MyClass& obj )
{
TheLibrary.ParentContext().CreateDynamicRecordAt( record, getRecordAttributeNames() );
TheLibrary.ParentContext().CreateConstIntegerCellAt( record->Elements(), obj.intAttr1 );
TheLibrary.ParentContext().CreateConstIntegerCellAt( record->Elements() + 1, obj.intAttr2 );
}3a. In some cases, if the object can be moved to a record, then: *
void CppToWafl( WAFL_ExternLib::Record* record, MyClass&& obj )
method should be implemented in a similar way; *
void CppToWafl( WAFL_ExternLib::Record* record, const MyClass& obj )
can be implemented to use the other one, like this:
static void CppToWafl( WAFL_ExternLib::Record* record, const MyClass& obj )
{
MyClass tmp = obj;
CppToWafl( record, std::move(tmp) );
}const char* WaflTypeName() returns a textual
specification of the corresponding Wafl record type. The usual form
is: static const char* WaflTypeName() {
return "Record[ intAttr2: Int, intAttr1: Int ]";
}const WAFL_ExternLib::RecordAttributes& getRecordAttributeNames()
returns an attribute names handler object. The name specified should be
ordered lexically. The usual form is: static const WAFL_ExternLib::RecordAttributes& getRecordAttributeNames()
{
static WAFL_ExternLib::RecordAttributes names {
"intAttr1",
"intAttr2",
};
return names;
}MyClass arguments
must be passed by value or by r-value reference
MyClass&&. The l-value references are not
allowed.MyClass results
must be passed by value.If all the requirements are fulfilled, the functions using
MyClass type as an argument or as a result are defined and
registered in the same way as any other function.
See examples in testLibExtern project for an
example.
Assume that the arguments will be deleted by caller. If the argument is to be returned, it must be copied.
Assume that the returned object will be deleted by caller after the usage. If an argument is to be returned, or some permanent object, then it must be copied.
Any class defined in the binary library may be exported as a
binary type. The only requirement is that it has to be
described by a corresponding descriptor object in the library object.
The descriptor object is created using
LibraryClassDescription template class in a form:
LibraryClassDescription< aType > aTypeDesc( "aTypeName" );When a function result is a binary type, the result is prepared using
SetResult method in the following form:
data.SetResult( new aType(...), aTypeDesc.GetData() );Please note:
new operator. It will be deleted by Wafl evaluator using
appropriate destructors and deallocators specified by the provided
descriptor.The only required method of the binary type class is
fString print() const method, which is used to create a
display-ready string representation of the object.
Each binary library must define a catalog of its content. The catalog informs the Wafl evaluator, which uses the library, on the function names, types and implementations.
The catalog has to be implemented by
LibraryImplementation::InitLibrary method of a predefined
class LibraryImplementation. The class and its singleton
object are defined in ExternLibBase.h header file, and the
initialization method is implemented by the library.
The InitLibrary method is invoked on an already created
LibraryImplementation object and it should populate it with
the functions data. The concept is simple and based on idea that each
chunk of information is inserted into the catalog using insertion
operator <<:
const char* : *this << "A Binary Library"VersionNumber object: *this << VersionNumber( 0, 6, 5 )LibraryClassDescription<...> object: *this << aBinaryTypeDesctLibFnData object. It can be exported in its
canonical form: *this << tLibFnData {
"maxInt", // exported fn. name
canonicalMaxIntFn, // canonical fn. implementation
"( Integer * Integer -> Integer )", // fn. type
"Returns a greater of two numbers." // fn. description
}It is easier to use macro LIB_FN_DATA, which creates the
canonical version of the function on the fly, and prepares its
registration data:
*this << LIB_FN_DATA(
"maxInt", // exported fn. name
regularMaxIntFn, // regular fn. implementation
"( Integer * Integer -> Integer )", // fn. type, using Wafl notation
"Returns a greater of two numbers." // fn. description
}There is also even more advanced macro LIB_FN_DATA_T. It
does all the same like the previous one, but also automatically
generates the function type name:
*this << LIB_FN_DATA_T(
"maxInt", // exported fn. name
regularMaxIntFn, // regular fn. implementation
"Returns a greater of two numbers." // fn. description
} *this << tLibFnData {
"aMethod", // exported fn. name
canonicalMethodFn, // canonical fn. implementation
"( TypeName -> Integer )", // fn. type
"Maps object to integer" // fn. description
}There are macros to automatically create canonical functions for
methods. The first one is LIB_METHOD_DATA, which creates
the canonical function for the method on the fly, and prepares its
registration data:
*this << LIB_METHOD_DATA(
"aMethod", // exported fn. name
TT::method, // regular fn. implementation
"( TypeName -> Integer )", // fn. type
"Maps object to integer" // fn. description
}There is also even more advanced macro
LIB_METHOD_DATA_T. It does all the same like the previous
one, but also automatically generates the function type name:
*this << LIB_METHOD_DATA_T(
"aMethod", // exported fn. name
TT::method, // regular fn. implementation
"Maps object to integer" // fn. description
}Note: Static methods are registered as functions, not as methods.
Please see the examples for more details.
A binary library must include libExtern/ExternLibBase.h
from Wafl project. This header file defines all the library elements
described in this file.
A binary library must link libExtern library from Wafl
project. This library already includes the other required Wafl
libraries.
Here we present some simple examples. For more information please see
the source for testLibExtern project and for
libw* sub-projects.
As a first example, we will export a sqrt function. The
first option is to define a canonical function:
void sqrtFnC( EvCell* reserved, const EvCell* args )
{
auto result = sqrt( getArg<double>( &args[ 0 ] ) );
setResult( reserved, result );
}This function uses the predefined marshalling template functions
getArg and setResult. It may be registered
using:
*this << tLibFnData {
"sqrt", // exported fn. name
sqrtFnC, // canonical fn. implementation
"( Float -> Float )", // fn. type
"Returns a square root of the number."
}The easier option is to use generic canonical functions, defined by
wrappers. However, because the sqrt is overloaded, we
should be more explicit on the version, like in:
inline double sqrtFn( double x ) { return sqrt( x ); }
auto sqrtFnC = wrappedFn< decltype(sqrtFn), sqrtFn >;It is easier to use some macros, which do some of the preparations
“on the fly”. The basic macro is LIB_FN_DATA, which creates
the canonical version and prepares it registration data:
inline double sqrtFn( double x ) { return sqrt( x ); }
...
*this << LIB_FN_DATA(
"sqrt",
sqrtFn,
"( Float -> Float )",
"Returns a square root of the number."
}Even more advanced is macro LIB_FN_DATA_T. It does all
the same like the previous one, but also automatically generates the
function type name:
inline double sqrtFn( double x ) { return sqrt( x ); }
...
*this << LIB_FN_DATA_T(
"sqrt",
sqrtFn,
"Returns a square root of the number."
}…
…
This function creates and returns as a result a new binary type object:
class aBinaryType {
public:
aBinaryType( int n ) : Attr_( n ) {...}
~aBinaryType() {...}
int Attr_;
};
LibraryClassDescription<aBinaryType> aBinaryTypeDesc( "aBinaryType" );
aBinaryType* createBinaryObject( int x )
{
return new aBinaryType( x );
}The function uses an object of a binary type as an argument:
int useBinaryObject( const aBinaryType* obj )
{
return obj->Attr_;
}The function computes the sum of integer array elements:
fInteger getArraySum( const fArrayInt* arr )
{
fInteger sum = 0;
for( auto i : *arr )
sum += i;
return sum;
}The life of array argument object is managed by Wafl evaluator. If a pointer to the object is is stored internally by a library, then the evaluator must be informed of that:
arr->AddReference();When such rerefence is deleted, teh evaluator must be informed of that, also:
arr->Free();The function creates and returns an array of size random
integers in the range [0,range-1]:
fArrayInt* getRandomArray( int range, int size )
{
fArrayInt* arr = (fArrayInt*) TheLibrary.ParentContext().CreateUninitializedArrayCellPtr( size );
auto set = TheLibrary.ParentContext().CreateConstIntegerCellAt;
for( auto& i : *arr )
set( arr->getElementAddr( i ), rand() % range );
return arr;
}The library defined in previous examples may have the following catalog:
...
void LibraryImplementation::InitLibrary()
{
*this
<< "A Binary Library" // Library name
<< aBinaryTypeDesc // A binary type
// Functions...
<< tLibFnData{ "sqrt1", sqrtFn,
"( Float -> Float )" }
<< tLibFnData{ "maxInt", maxIntFn,
"( Integer * Integer -> Integer )" }
<< tLibFnData{ "stringAdd", stringAddFn,
"( String * String -> String )" }
<< tLibFnData{ "newBin", createBinaryObject",
"( Integer -> aBinaryType )" }
<< tLibFnData{ "useBin", useBinaryObject,
"( aBinaryType -> Integer )" };
}On some Linux distributions, when using the SDL library in Wafl, an error message like this may be displayed:
X Error of failed request: BadWindow (invalid Window parameter)
Major opcode of failed request: 25 (X_SendEvent)
Resource id in failed request: 0x1400009
Serial number of failed request: 9
Current serial number in output stream: 11
The problem is with the incorrect configuration of SDL. It should be
enough to install the libsdl2-dev package. Installing the
package will set the correct configuration of the library. After that,
the package can be removed, and the problems should not repeat.
sudo apt install libsdl2-dev
sudo apt remove libsdl2-dev
The tutorial is under construction.
Expect more content soon.