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

1 Introduction

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.

1.1 Command Line Interpreter

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.

1.2 Hello World

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!

1.3 Main Wafl Concepts

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.

1.4 Introduction to the Types

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.

2 Program Structure

2.1 Program Is an Expression

In Wafl, programs are expressions. Each program (or expression) primarily defines what is to be evaluated and not how it is to be executed or evaluated.

If we want to write a program that computes 1+2+3, then we only write that expression and nothing else:

1+2+3

6

We can use literals, names, operators and functions in expressions. Here are a few simple examples. First, this is an integer expression:

53 / 5 * 4 + 12 % 10 + abs(-7)

49

A float expression:

53. / 5. * 4. + 2.12 + sin(1.34)

45.49348454169532

A string expression:

strUpperCase( "Hello " + 'world!' )

HELLO WORLD!

A Boolean expression:

false or true

true

Wafl supports many of the usual operators and functions for primitive types (integer, float, string and bool). There are also more complex types (list, array, map, tuple, record), which we will discuss in more detail later.

Simple expressions are often not enough. In the following sections, we will see how you can define and use functions and named expressions.

2.2 Comments

There are two types of comments in Wafl. Block comments start with /* and end with */. Line comments start with // and end with the end of the line.

/* This is a block comment,
   in two lines */
1 + 2 // This is a line comment after an expression

3

2.3 Tuples

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 #}

2.4 Local Definitions

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.

2.5 Function Definition

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:

For example, we could not use:

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;
    };
}

*---************

2.6 Named Expression Definition

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:

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;
    };
}

*---------************************

2.7 No Variables

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:

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.

2.8 The Order of the Definitions

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.

2.9 Conditional Expression if

In imperative programming languages, it is common to use conditional statements to control the flow of program execution. Wafl is a functional language and does not contain statements, so in Wafl we use conditional expressions instead.

The Wafl programming language has two types of conditional expressions: if and switch. The expression if contains a condition and two optionally evaluated expressions:

if <condition> then <then-exp> else <else-exp> 

where <condition> must be an expression of the logical type Bool, while the expressions <then-exp> and <else-exp> can be of any type, but must have the same type.

The conditional expression is evaluated first. If its value is true, then the expression <then-exp> in the then branch is evaluated and the resulting value is the result of the full if expression. In the other case, if <condition> has the value false, then the expression <else-exp> in the else branch is evaluated and the resulting value is the result of the complete if expression.

{#
    if 5 > 2 then 'five' else 'two',
    if 5 < 2 then 'five' else 'two'
#}

{# 'five', 'two' #}

2.10 Conditional Expression switch

The conditional expression if evaluates one of the two alternative expressions. If more alternatives are required, we can use multiple if expressions or a switch-expression.

The switch expression consists of a conditional expression, at least one case clause and a mandatory default clause:

switch <cond-exp> {
    <case-clause>;
    <case-clause>;
    ...
    <default-clause>
}

The conditional expression is evaluated first. Then, based on the resulting value, one of the case clauses is selected and evaluated, or, if none is selected, the default clause is evaluated. The result of the selected and evaluated clause is the result of the switch expression.

Each case clause begins with the keyword case, followed by a comma-separated list of values. The list of values is followed by an expression that ends with a semicolon:

case <literal>, ... <literal> [:] <exp>;

An optional colon symbol can separate the value list from the clause expression. In addition, the keyword case can be used as a separator instead of a comma and an optional colon symbol can be used in front of it. So, case 1,2,3 is the same as case 1: case 2: case 3.

The default clause is mandatory. It is defined as:

default [:] <exp> [;]

The following example uses both types of conditional expressions to calculate how many days a given month has:

{#
    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.

2.11 Recursion

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:

  1. the recognition of the terminal condition, i.e. the case where the arguments allow a non-recursive, direct evaluation of the result, and

  2. 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.

2.12 Libraries

Wafl libraries are used by declaring a local library name and binding it to an externally defined library.

Wafl Library Definition

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 ;
}

Library Declaration

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.

Referencing Library Elements

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"

Importing a Library

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:

Importing a Library Into the Program Namespace

From version 0.6.10, the using syntax is supported. It is similar to the importing syntax with two significant differences:

It is still possible to use the full library name referencing as with import and the library declaration.

Library Documentation

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:

Parametrized Libraries

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 Libraries

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:

To 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.

Simple Universal Library Declaration

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;

Library Search Path

The libraries are searched for in the following directories:

where:

3 Programming With Functions

3.1 Strict Type Checking

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.

3.2 Automatic Type Inference

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:

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:

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.

3.3 Polymorphism

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:

  1. it can be applied to arguments of any value type (Integer, Float or String);
  2. both arguments must be of the same value type and
  3. the result is of the same value type.

Similarly, the type of the function sub is inferred to be (Numeric['1] * Numeric['1] -> Numeric['1]). Thus:

  1. it is applicable to arguments of any numeric type (Integer or Float);
  2. both arguments must be of the same type and
  3. the result is of the same type.

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:

  1. it is applicable to three arguments of any numeric type (Integer or Float);
  2. all arguments must be of the same type and
  3. the result is of the same type.

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.

3.4 Higher Order Functions

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:

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.

3.5 Partial Application

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.

3.6 Lambda Functions

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.

3.7 Lambda Closures

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 #}

3.8 Operators as Functions

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.

3.9 Dot Operator

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().

Transformational Semantics

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.

Continuation Operator

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() 
+ 1

3
4

Alternative Syntax - Arrow Operator

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 ‘=>’.

3.10 Explicit Computation State

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.

3.11 Cached Functions

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.

4 Primitive Types

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.

4.1 Literals

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' #}

4.2 Operators

4.2.1 Integer operators

Wafl has the usual arithmetic operators:

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++:

{#
    // '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.

4.2.2 Float Operators

Wafl has the usual float operators:

{#
    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 #}

4.2.3 String operators

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.

4.2.4 Bool operators

The usual logical operators are supported in both C-like and SQL-like syntax:

{#
    true or false,
    true || false,
    true and false,
    true && false,
    not true,
    !true
#}

{# true, true, false, false, false, false #}

4.2.5 Comparison Operators

The usual comparison operators are defined for the types Integer, Float and String:

{#
    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.

4.3 Conversion Functions

Wafl is a strongly typed programming language and no implicit type conversions are allowed. Therefore, the Wafl core library contains the conversion functions:

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.

4.3.1 Conversion to Integer

The function asInt(x) converts every non-integer primitive value x into the Integer type:

For the conversion from Float to Integer, there are also:

There are also functions for converting String values to Integer:

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 #}

4.3.2 Conversion to Float

The function asFloat(x) converts every primitive non-float value x into the type Float:

{#
    asFloat(7),
    asFloat('6.2'),
    asFloat('abc'),
    asFloat(true),
    asFloat(false)
#}

{# 7., 6.2, 0., 1., 0. #}

4.3.3 Conversion to String

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:

{#
    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' #}

4.3.4 Conversion to Bool

The function asBool(x) converts every primitive non-bool value x into the type Bool:

{#
    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 #}

4.4 Integer Functions

Wafl core library includes three integer functions:

4.4.1 Integer Function abs

The integer function abs(x) computes an absolute integer value of the given integer value x:

{#
    abs( 123 ),
    abs( -123 ),
    abs( -0 )
#}

{# 123, 123, 0 #}

4.4.2 Integer Function sgn

The 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 #}

4.4.3 Integer Function between

The 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 #}

4.4.4 Function random

The 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 #}

Randomizing

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.

4.5 Float Functions

The Wafl core library contains the following float functions:

The following conversion functions have already been presented in the previous sections:

4.5.1 Float Function abs

The 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. #}

4.5.2 Float Function sgn

The 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. #}

4.5.3 Float Function between

The 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 #}

4.5.4 Float Function roundTo

The 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. #}

4.5.5 Function exp

{#
    exp( -10.0 ),
    exp( 0.0 ),
    exp( 1.0 ),
    exp( 10.0 )
#}

{# 4.5399929762484854e-05, 1., 2.718281828459045, 22026.465794806718 #}

4.5.6 Function ln

The 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 #}

4.5.7 Function log

The 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. #}

4.5.8 Function log2

The 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 #}

4.5.9 Function pow

The 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. #}

4.5.10 Function sqrt

The 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 #}

4.5.11 Trigonometric functions

The following trigonometric functions are available:

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 #}

4.6 String Functions

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.

4.6.1 Basic String Functions

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

strLen

The 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.

strCat

The 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, ifNull

Due 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.

4.6.2 String Extraction Functions

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 subStr

The 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:

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 strRight

The strLeft(s,n) function returns a substring containing the first n characters of the string s:

The strRight(s,n) function returns a substring containing the last n characters of the string s:

{#
    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 strTrim

The 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' #}

4.6.3 String Index and Slice Operators

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:

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.

4.6.4 String Search Functions

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 strNextPosI

The 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 strNextLastPosI

The 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 strEnd

The 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 strHasSubI

The 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.

4.6.5 Substring Counting Functions

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.

4.6.6 String Replace Functions

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.

4.6.7 Functions strSplit... and strJoin

Here 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.

4.6.8 Encoding functions

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&amp;&lt;&gt;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 \'\"...

4.6.9 Other String Functions

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.

4.6.10 UTF-8 Functions

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.

BOM 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 #}

UTF-8 Validity Functions

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' #}

utfLen

Function / 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 sortUtf8

Function / 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).

utfAt

Function / 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, utfRight

Function / 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' #}

Other UTF-8 Functions

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' #}

Using Regular String Functions

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.

5 List Type

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.

5.1 List Literals

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:

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.

5.2 Basic List Functions

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.

List Comparison

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 nonEmpty

To 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 longerThan

The 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:B

The 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]] #}

5.3 Basic List Processing

List elements are usually processed using recursion:

We will try to apply this concept in some examples.

A List Sum

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

A List of Squares

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:

[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]

An Integer Range

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]

5.4 Advanced List Processing

In the examples of the previous section, we can recognize a certain repetitiveness. Let us focus on two topics:

  1. how to process the head element and
  2. how to combine the result of processing the head with the results for the rest of the list (tail).

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.

5.5 More List Functions

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, subList

The 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], [] #}

List Index and Slice Operators

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 appendAll

Function / 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, intRangeBy

Function / 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] #}

5.6 Functions map and mapIdx

Function / 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)

map

The 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.

mapIdx

Sometimes 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']

5.7 Functions zip and cross

Function / 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)

zip

The 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' #}]

zipBy

The 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']

zipByIdx

The 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']

cross

The 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' #}]

crossBy

The 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']

crossByIdx

The 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']

5.8 Functions foldr, foldl and fold

Function / 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

foldr

Like 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.

foldl

If 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)

A Few Examples

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]

fold

If (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.

5.9 Selection Functions

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_par

The 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.

selectN

The 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.

selectFrom

The 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] #}

selectWhile

The 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] #}

selectUntil

The 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] #}

selectByIdx

The 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], [], [] #}

selectTrue

The 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], [], [] #}

selectDistinct

The 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().

selectMap

The 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] #}

mapSelect

The 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 mapFilter

The functions filter, filter_seq, filter_par, filterN, filterMap and mapFilter are deprecated synonyms for select, select_seq, select_par, selectN, selectMap and mapSelect.

5.10 Functions for Finding Elements

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.

findFirst

The 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 #}

find

The 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], [] #}

findN

The 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], [] #}

findEqFirst

The 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 #}

findEq

The 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], [] #}

findEqN

The 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], [] #}

5.11 Functions forall and exists

Function / 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.

5.12 Functions count and countRep

Function / 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

5.13 Functions sort and sortBy

Function / 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.

5.14 Functions in and contains

Function / 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 #}

5.15 Lazy Lists

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.

6 Structured Types

6.1 Array Type

The array is a sequence type, like a list, but with a focus on the efficiency of indexing and slicing operations.

6.1.1 Array Literals

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.

6.1.2 Common Sequence Functions

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.

6.1.3 Functions newArray and newArrayFn

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 #]

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 #] #}

6.1.4 Array Conversion Functions

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 #}

6.2 Map Type

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.

6.2.1 Creating a Map

Function createMap

There 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.

Function groupBy

If 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] #}

6.2.2 Using a Map

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.

Functions keys and values

Function / 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 #}

Functions hasKey, hasValue, findKey and findValue

Function / 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 #}

6.3 Tuple Type

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.

6.3.1 Tuple Literals

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]]]
]

6.3.2 Tuple Selectors

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.

6.3.3 Tuple Updater

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.

6.3.4 Tuple Type-Checking

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 ].

6.4 Record Type

Record is a structured data type. It consists of a series of named values. Record elements can have different types.

6.4.1 Record Literals

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' } } }

6.4.2 Record selectors

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.

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:

6.4.3 Record Updaters

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 } #}

6.4.4 Record Type-Checking

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])

6.4.5 A Record Example

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

7 Databases

7.1 Database Connection

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.

7.1.1 Command Line Database Connection Parameters

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>.

Specifying 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.

7.1.2 Configuration File Database Parameters

Some of the database connection parameters can be set using configuration files. These parameters are located in the [database] section:

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 = untyped

For more detail please see the Configuration Files section.

7.2 Queries

7.2.1 Query Functions

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
};

Untyped Queries

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.

Typed 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

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.

Dynamic Queries

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]]]).

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.

Lazy Semantics

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.

7.3 Transactions

7.3.1 Transactional Functions

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

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

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.

Action Statements

…todo…

Example

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;
};

7.3.2 …

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.

8 Elements of Wafl Library

8.1 Program Control

Side Effects

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 ...

Iteration Functions

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]

8.2 File Reading

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

8.3 File Writing

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. 

8.4 File Operations

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.

8.5 Directory Operations

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.

8.6 Regex Functions

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.

Match the Entire String

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 #}

Match the Substrings

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] #}

Replace the Substrings

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']] #}

8.7 Command Line

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.

8.7.1 Program Arguments

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().

8.7.2 Program Result

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

8.7.3 User Interaction

Command Line Output

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.

Command Line Input

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!

8.7.4 Shell Command Execution

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.

8.8 Web and HTTP Functions

The HTTP and web functions of the Wafl library are divided in two parts:

Universal HTTP Functions

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

Web Functions

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.

8.9 Wafl to JSON

Convert Any Value to JSON

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'
    ]
 } ]

8.10 Wafl Program Evaluation

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

9 Parallelization

9.1 Wafl and Parallelization

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.

Explicit Parallelization

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:

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

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:

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.

Implicit Parallelization Functions

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:

An alternative is to define a new function parallel_f as follows:

parallel_f = parallel(f);

Implicit Parallelization Command Line Options

The configuration of the parallelization engine can be set via command line options:

Explicit Sequential Functions

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:

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.

9.2 Parallel Functions

Isolation

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.

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:

If we want the fold to be evaluated in parallel, there is an additional condition:

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.

Suggesting and Preventing Parallelization

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.

10 Wafl Library Reference

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.

10.1 Core Library

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.

10.1.1 Elementary Library

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.

10.1.2 Conversion Library

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.

10.1.3 Sequence Library

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.]

10.1.4 List Library

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.

10.1.5 Array Library

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 #]

10.1.6 String Library

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.

10.1.7 Regex Library

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.

10.1.8 File Library

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 )

10.1.9 Command Line Library

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.

10.1.10 Web Library

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.

10.1.11 Parallelization Library

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.

10.1.12 XML Library

This library is currently under development.

10.1.13 Meta Library

Function / Type and Description

10.2 Binary Libraries

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.

10.2.1 Curl Library

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.

10.2.2 Curl Library

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.

10.2.2 Drawing Library (SDL)

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.

10.2.3 Timer Library

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.

10.2.4 Edlib Library

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.

10.3 Wafl Libraries

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.

10.3.1 Colors Library

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)

10.3.1 Console Library

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.

10.3.1 Csv Library

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.

10.3.1 Paths Library

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.

10.3.1 Replace Library

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.

10.3.1 Test Library

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.

10.3.1 Test Runner Library

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.

11 Command Line Reference

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.

11.1 Command Line Options

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`.

11.2 Configuration Files

Configuration files are defined at three basic levels:

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
...

Sections and Parameters

The following sections are defined:

Other sections can be defined in future.

Section command-line

For each command line option -option and parameter -param:value there is a corresponding parameter in the command-line section:

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 = 0

Section jit-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 = 1

Please see JIT documentation for details.

Section jit-msvc

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.lib

#keep-source = 1

Section database

Here 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 = untyped

Please see Configuration File Database Parameters for details.

11.3 ANSI Control Codes

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';
}

12 Advanced

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.

12.1 JIT

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:

Options

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.

Configuration

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 //.

Linux Options

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.

Linux Include Search Path

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/include
Linux Compiler Options

Compiler 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 -DNDEBUG
Linux Linker Options

Linker 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;
Linux Keep Source Files

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.

A Complete Linux Example

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;

Windows MSVC Options

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.

Windows Tools Search Path

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\x64
Windows Include Search Path

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. 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\include
Windows Library Search Path

Library 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\lib
Windows Compiler Options

Compiler 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 /O2
Windows Linker Options

Linker 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.lib
Windows Keep Source Files

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.

A Complete Window Example

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.lib

12.2 Wafl Binary Libraries

Wafl 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:

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.

WaflCore

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:

Glossary

1. Using Binary Defined Libraries

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.so

For more detailed descriptions, use ??:

    clwafl ?? Regex

The import syntax is also available.

2. Defining Functions

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.

3. Defining Canonical Functions

This section is not important if canonical functions are built automatically.

Each canonical function implementation has to do three jobs:

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.

4. Arguments

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:

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.

5. Result

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.

6. Function Types

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.

7. Supported Arguments and Result Types

7.1 Primitive NonString Types

All primitive types are supported:

NOTE: If the canonical versions of the functions and/or the function types are automatically created, then the following additional constraints hold:

7.2 Using String type

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 argument

So, 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 result

Using 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.

7.3 Arrays

The following array types are supported:

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.

7.4 Records

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:

  1. the specialization of class template template<> class RecordMarshalling<MyClass> with four static methods has to be implemented
  2. MyClass WaflToCpp( const WAFL_ExternLib::Record* record ) for conversion of arguments of a Wafl record type to objects of the MyClass class.
    static MyClass WaflToCpp( const WAFL_ExternLib::Record* record )
    {
        MyClass obj {
            getArg_safe<int>( record->ElementByName( "intAttr1" )),
            getArg_safe<int>( record->ElementByName( "intAttr2" ))
        };
        return obj;
    }
  1. void CppToWafl( WAFL_ExternLib::Record* record, const MyClass& obj ) for conversion of function results of the MyClass type to a Wafl record type.
    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) );
    }
  1. 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 ]";
    }
  1. 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;
    }
  1. In implemented library functions, the MyClass arguments must be passed by value or by r-value reference MyClass&&. The l-value references are not allowed.
  2. In implemented library functions, the 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.

7a. Arguments and Result Lifetime

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.

8. Exported Types

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:

  1. A returned object must be created dynamically, using new operator. It will be deleted by Wafl evaluator using appropriate destructors and deallocators specified by the provided descriptor.
  2. The type descriptor is used to provide the information on the appropriate destructors and deallocators.
  3. No binary type object may be created or destroyed directly by Wafl evaluator. They may be created in binary library only.
  4. Arguments of binary types constructors should never be deleted by the library; only the caller should delete them. This is important, because if they are provided by Wafl evaluator, then only the evaluator may delete them.

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.

9. Library Catalog

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 <<:

  1. The library name is registered by inserting a const char* :
    *this << "A Binary Library"
  1. The minimal caller version number is registered by inserting a VersionNumber object:
    *this << VersionNumber( 0, 6, 5 )
  1. Each of the exported classes (i.e. library defined Wafl object types) is registered by inserting the corresponding LibraryClassDescription<...> object:
    *this << aBinaryTypeDesc
  1. Each of the exported functions is registered by inserting the corresponding tLibFnData 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
    }
  1. The methods of the registered classes can be registered in a similar way. To export a method, an appropriate canonical function has to be provided. When a method is registered, it is used in Wafl as a function, where an appropriate object is added as a first argument. So, a method registration is almost the same as in the case of the functions:
    *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.

10 Headers

A binary library must include libExtern/ExternLibBase.h from Wafl project. This header file defines all the library elements described in this file.

11 Linking

A binary library must link libExtern library from Wafl project. This library already includes the other required Wafl libraries.

12 Examples

Here we present some simple examples. For more information please see the source for testLibExtern project and for libw* sub-projects.

1. A simple unary function

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."
    }

2. An integer binary function

3. A binary String function

4. A binary type object constructor

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 );
    }

5. A binary type object as argument

The function uses an object of a binary type as an argument:

    int useBinaryObject( const aBinaryType* obj )
    {
        return obj->Attr_;
    }

6. Array as argument

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();

7. Array as result

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;
    }

8. A library catalog

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 )" };
    }

13 Issues

13.1 Linux Issues

Misconfiguration of SDL

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

14 More Content…

The tutorial is under construction.

14.1 Soon…

Expect more content soon.