Table of Contents

1 Introduction

1.1 Command Line Interpreter

1.2 Hello World

1.3 Main Wafl Concepts

1.4 Introduction to the Types

2 Program Structure

2.1 Program Is an Expression

2.2 Comments

2.3 Tuples

2.4 Local Definitions

2.5 Function Definition

2.6 Named Expression Definition

2.7 No Variables

2.8 The Order of the Definitions

2.9 Conditional Expression if

2.10 Conditional Expression switch

2.11 Recursion

2.12 Libraries

3 Programming With Functions

3.1 Strict Type Checking

3.2 Automatic Type Inference

3.3 Polymorphism

3.4 Higher Order Functions

3.5 Partial Application

3.6 Lambda Functions

3.7 Lambda Closures

3.8 Operators as Functions

3.9 Dot Operator

3.10 Explicit Computation State

3.11 Cached Functions

4 Primitive Types

4.1 Literals

4.2 Operators

4.3 Conversion Functions

4.4 Integer Functions

4.5 Float Functions

4.6 String Functions

5 List Type

5.1 List Literals

5.2 Basic List Functions

5.3 Basic List Processing

5.4 Advanced List Processing

5.5 More List Functions

5.6 Functions map and zip

5.7 Functions foldr, foldl and fold

5.8 Functions filter, find and filterMap

5.9 Functions forall and exists

5.10 Functions count and countRep

5.11 Functions sort and sortBy

5.12 Functions in and contains

5.13 Lazy Lists

6 Structured Types

6.1 Array Type

6.2 Map Type

6.3 Tuple Type

6.4 Record Type

7 Elements of Wafl Library

7.1 Program Control

7.2 File Reading

7.3 File Writing

7.4 File Operations

7.5 Directory Operations

7.6 Regex Functions

7.7 Command Line

7.8 Web and HTTP Functions

7.9 Wafl to JSON

7.10 Wafl Program Evaluation

8 Parallelization

8.1 Wafl and Parallelization

8.2 Parallel Functions

9 Core Library Reference

9.1 Main Library

9.2 Command Line Library

9.3 Web Library

9.4 XML Library

9.5 Drawing Library (SDL)

9.6 Timer Library

9.7 Edlib Library

10 Command Line Reference

10.1 Command Line Options

10.2 Configuration Files

10.3 ANSI Control Codes

11 Advanced

11.1 JIT

11.2 Wafl Binary Libraries

12 More Content…

12.1 Soon…

 

 

Last update: 29.01.2025.

Wafl

Wafl

Tutorial / 2 - Program Structure

Open printable version

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

A string expression:

strUpperCase( "Hello " + 'world!' )

HELLO WORLD!

A boolean expression:

false or true

true

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

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

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:

  • a named expression is a definition in which a name is defined to represent the value of a given expression, and
  • a function is a definition in which a name is defined to represent a mapping of the given arguments to the value of the given expression.

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:

  • global names;
  • function arguments;
  • all functions defined in its own where-block, or in some of the parent where-blocks (including its own name) and
  • all named expressions defined in its own where-block or in the where-block of the main expression.

In the next example, we can use the following in the expression body of the function f:

  • named definition name_main, defined in the main where-block;
  • named definition name_f, defined in its own where-block;
  • its own argument x;
  • function f1, defined in its own where-block;
  • function g, defined in a parent where-block.

For example, we could not use:

  • function g1 in f, because it is not defined in a fs parent where-block;
  • name name_f in f1, because it is not defined in f1s own where-block nor in main where-block.

f('*')
where {
    f(x) = x + name_main + name_f + f1(x) + g(x)
    where {
        name_f = x + x;
        f1(x) = x + x + g(x);
    };
    name_main = '---';    
    g(x) = name_g + g1(x)
    where {
        name_g = x + x;
        g1(x) = x + x;
    };
}

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

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:

  • global names;
  • all functions defined in some of the parent where-blocks;
  • all named expressions (other than itself) defined in its direct parent where-block (the one in which it is defined), or in main expression where-block and
  • arguments of the function in whose where-block it is defined.

In the following example, we can use the following in the definition of the name name_f:

  • named definition name_main, defined in the main where-block;
  • named definition name_fa, defined in its direct parent where-block;
  • argument x, of the function f in whose where-block it is defined;
  • function f1, defined in a parent where-block;
  • function g, defined in a higher level parent where-block.

It is important to note that we cannot use recursive references to name definitions (either directly or indirectly) within a name definition expression, but we can use recursive references to a function in whose where-block the named expression is defined.

f('*')
where {
    f(x) = x + name_main + name_f + f1(x) + g(x)
    where {
        name_fa = name_main + x + x;
        name_f = name_main + name_fa + g(x) + f1(x) + x + x;
        f1(x) = x + x + g(x);
    };
    name_main = '---';    
    g(x) = name_g + g1(x)
    where {
        name_g = x + x;
        g1(x) = x + x;
    };
}

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

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_867226_0.tmp
Parser error [C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_867226_0.tmp]
    Definition name repeated! [err 1211:3]
    Line 5:     x = 2;
            ___/
--- End loading: C:\Users\smalkov\AppData\Local\Temp\wafltmpfile_867226_0.tmp

A name can have different definitions in different scopes, even if one of the scopes is contained within another. If more than one definition of the same name is visible, the one with a narrower scope (i.e. closer to the place of use) is relevant.

In the next example, the name x is defined twice:

  • the first definition is visible in the main expression and all its subdefinitions, including the body of the function f);
  • the second definition is visible in the body of the function f and in all its subdefinitions;
  • this is valid;
  • the second definition has the narrower scope, so it is used in the body of the function f;
  • the first definition is used in the main expression because the second one not visible there.

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

{# 780, 780, 780, 780, 780, 780, 780, 780, 780, 780 #}

Future Wafl versions might analyze whether the definition body is deterministic or non-deterministic. If the definition is non-deterministic, then its semantics can be defined differently, so that it is re-evaluated each time. In that case, the previous example might give different results. Non-deterministic behavior would include the use of the random function, but also the use of files, networks and so on.

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:

  • The conditional expression and all literals in all case clauses must be of the same primitive type.
  • All other expressions (case clause expressions and the default expression) must be of any but same type.
  • If the same literal is specified several times, only the first occurrence is taken into account.

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

Declaration of the Library

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 file '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 file 'v1/example.wlib';
elib2 = library file '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"

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:

  • documentation of the library declaration and
  • documentation of the public definitions.

Library names and definition names and types are automatically extracted from the code. Definition descriptions are extracted from the comments in a Doxygen-like manner:

  • The supported formats for documenting comments are:

    • /// for single-line comments and

    • /** ... */ for multi-line comments.

  • All documenting comments that precede a library declaration or a function definition are extracted and used as the corresponding descriptions.

  • Markdown elements can be used in the documenting comments.

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

Dynamic Libraries

Dynamic Wafl Libraries are libraries written in C and C++. The dynamic libraries are distributed as .so or .dll files. They are used in a similar way to regular Wafl libraries, but with a specific declaration syntax:

<name> = library extern [cpp] <libname>

where <libname> can be an exact library file name or a generic library name. The Wafl loader attempts to load one of the following files, depending on the platform:

  • <libname>
  • <libname>.so
  • libw<libname>.so
  • <libname>.dll
  • libw<libname>.dll

To list the contents of a dynamic library, use clwafl with the command line option -listlib:<libname>.

The development of dynamic libraries is outside the scope of this tutorial. Please contact the developers for more information.

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 dynamic library libwAlib.dll (or libwAlib.so); if no binary library is found, a regular Wafl library Alib.wlib is used:

MyLib = library 'Alib';

If <libfilename> is not specified, the library name <libname> is used by default. In the following example, it is the same as if the library file name is specified as 'MyLib', so that a binary dynamic library libwMyLib.dll (or libwMyLib.so) is searched for first; if no binary library is found, then a regular Wafl library MyLib.wlib is used.

MyLib = library;

Library Search Path

The libraries are searched for in the following directories:

  • <ref>
  • <ref>/lib
  • <libdir>
  • ${WAFL_PATH}/bin (only for dynamic libraries)
  • ${WAFL_PATH}/bin/lib (only for dynamic libraries)
  • ${WAFL_PATH}/lib
  • <prg>
  • <prg>/lib
  • <prg>/bin (only for dynamic libraries)

where:

  • <ref> is the location of the file from which the library is referenced;
  • <libdir> is the location specified as the -libdir interpreter option;
  • WAFL_PATH is an environment variable pointing to the root directory of the Wafl package installation;
  • <prg> is the location where the currently used Wafl program module (interpreter) is located.

Wafl Home            Downloads            Wafl Tutorial v.0.6.8 © 2021-2025 Saša Malkov            Open printable version