Table of Contents
Last update: 29.01.2025.
The array is a sequence type, like a list, but with a focus on the efficiency of indexing and slicing operations.
The general array type is written as Array['1]
, where
'1
denotes a type variable (a pure polymorphic
type). Each specific array in a program must have a specific type,
where this free type is fully defined.
The syntax of array literals is similar to that of list literals. The only difference is that square brackets with hash symbols are used instead of square brackets.
{#[# #],
[#1,2,3#], // Array[Int]
[#'a','b','c','d'#], // Array[String]
[#[#1,2#],[#3,4,5#],[#6,7,8#]#] // Array[Array[Int]]
#}
{# [# #], [# 1, 2, 3 #], [# 'a', 'b', 'c', 'd' #], [# [# 1, 2 #], [# 3, 4, 5 #], [# 6, 7, 8 #] #] #}
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)
filter
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2]['1])
Extracts the sequence elements which satisfy the given
condition
filterN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2]['1])
Extracts at most N elements which satisfy the given condition.
filterMap
(Sequence['3]['1] * ('1 -> Bool) * ('1 -> '2) -> Sequence['3]['2])
Maps a function to sequence elements which satisfy the condition.
mapFilter(seq,cond,fn) == map(filter(seq,cond),fn)
find
(Sequence['2]['1] * ('1 -> Bool) -> Sequence['2][Int])
Finds the indexes of the elements which satisfy the given
condition.
findN
(Sequence['2]['1] * ('1 -> Bool) * Int -> Sequence['2][Int])
Finds at most N indexes of the elements which satisfy the given
condition.
forall
(Sequence['2]['1] * ('1 -> Bool) -> Bool)
Checks whether the given condition applies to all sequence
elements:
forall(list,cond).
exists
(Sequence['2]['1] * ('1 -> Bool) -> Bool)
Checks whether any sequence element fulfills the condition:
exists(sequence,cond)
in
('1 * Sequence['2]['1] -> Bool)
Checks whether
the element is in the sequence:
el.in(seq)
contains
(Sequence['2]['1] * '1 -> Bool)
Checks whether
the element is contained in the sequence:
seq.contains(el)
count
(Sequence['2]['1] * ('1 -> Bool) -> Int)
Counts the sequence elements that fulfill the condition:
count(seq,cond)
sort
(Sequence['2][Value['1]] -> Sequence['2][Value['1]])
Sorts value sequence.
sortBy
(Sequence['2]['1] * ('1 * '1 -> Bool) -> Sequence['2]['1])
Sorts the sequence using given comparator.
append
(SequenceStr['1]['2] * SequenceStr['1]['2] -> SequenceStr['1]['2])
Appends the second sequence to the first sequence.
appendAll
(Sequence['1][SequenceStr['2]['3]] -> SequenceStr['2]['3])
Appends all sequences in the given sequence.
groupBy
(Sequence['3]['1] * ('1 -> '2) -> Map['2][List['1]])
Creates a catalog from the sequence. Each catalog element is a list
of all elements mapped to the key value by the given function.
countRep
(Sequence['2]['1] -> Map['1][Int])
Creates a
catalog from the sequence. Keys are the distinct elements, and values
are the number of occurrences.
These functions have already been introduced for the list type and will not be discussed here again. Their application to arrays is identical to that for lists. The only difference is that the arguments and the results are arrays instead of lists.
newArray
and 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 #], [# 2, 0, 0, 0, 2 #] #}
Arrays and lists are similar, but not the same types. They are both sequences, but have a different internal structure. Arrays are assumed to have a fixed content, which is usually used for indexing and slicing, while lists are often used by decomposing into head and tail. The usual sequence operations work the same for both.
Furthermore, the internal implementation of the list type can be the same as that of the array type. Depending on the context and the creation method used, the interpreter decides whether a list is structured as usual or as an array.
For this reason, there are functions for converting a list into an
array and vice versa. To force the internal array-like organization of a
list, convert it into an array and then back into a list:
lst.asArray().asList()
.
Function / Type and Description
asArray
(Sequence['1]['2] -> Array['2])
Converts a
sequence to an array
asList
(Sequence['1]['2] -> List['2])
Converts a
sequence to a list
{#[].asArray(),
[##].asList(),
[1,2,3].asArray(),
[1,2,3].asArray().asList(),
['1','2','3'].asArray(),
['1','2','3'].asArray().asList(),
[#1,2,3#] == [#1,2,3#],
[#1,2,3#] == [#1,2,3,4,5#],
[#1,2,3#] != [#1,2,3#],
[#1,2,3#] != [#1,2,3,4,5#]
#}
{# [# #], [], [# 1, 2, 3 #], [1, 2, 3], [# '1', '2', '3' #], ['1', '2', '3'], true, false, false, true #}
The map is an indexable type. It has a catalog structure in which the elements are accessed via keys. Both the keys and the values can be of any valid type.
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:
{#['a'],
aMap
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.
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:
[1] #}
{# aMap, aMapwhere {
aMap = (1..20).groupBy(\x:x%4);
}
{# [<1,[1, 5, 9, 13, 17]>, <2,[2, 6, 10, 14, 18]>, <3,[3, 7, 11, 15, 19]>, <0,[4, 8, 12, 16, 20]>], [1, 5, 9, 13, 17] #}
The usual way to use the maps is to index them. However, if it is not clear which exact key values are allowed, some other usage methods may be useful.
keys
and
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 n
th key (or value) from the corresponding arrays. They
are equivalent to indexing the arrays, such as keys()[n]
and values()[n]
.
{#keys(),
aMap.key(0),
aMap.values(),
aMap.value(0)
aMap.
#}where {
aMap = createMap([# 'a', 'b', 'c' #], [# 11, 22, 33 #]);
}
{# [# 'a', 'b', 'c' #], 'a', [# 11, 22, 33 #], 11 #}
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.
{#['a'],
aMaphasKey('a'),
aMap.hasKey('A'),
aMap.['c'],
aMapfindKey('c'),
aMap.findKey('C')
aMap.
#}where {
aMap = createMap([# 'a', 'b', 'c' #], [# 11, 22, 33 #]);
}
{# 11, true, false, 33, 2, -1 #}
Tuple is a structured data type. It consists of a series of unnamed elements that are referenced by a position. Tuple elements can have different types.
Tuples are written with curly brackets and hashes:
{# ... #}
, and tuple elements are separated by commas. The
elements can be listed as literals or as expressions.
Tuples must not be empty.
It is permitted to specify the element separator after the last element.
Although the syntax of a hanging separator may seem too loose, it is very useful in practice. Tuple literals are often written with one element in a line (see the following example). If a hanging separator were not allowed in such cases, commenting the last element would require deleting or commenting the previous separator.
In the following example there is a tuple with three tuple elements of different types.
{#1 #},
{# 1 + 2,'a'+'b' #},
{# 1,{# 'list',[1,2,3] #}#},
{# // 'last' - this element is declared as a comment
// and the previous separator `,` is hangs, but that's OK
#}
{# {# 1 #}, {# 3, 'ab' #}, {# 1, {# 'list', [1, 2, 3] #} #} #}
The type of this tuple is:
Tuple[
Tuple[Integer],
Tuple[Integer, String],
Tuple[Integer,Tuple[String,List[Integer]]]
]
Tuple elements are accessed via tuple selectors. As tuple elements are not named, their position is used to select them. The positions of the tuple elements are 1-based.
Tuple selectors use the dot-syntax
<tuple>.<n>
, where a positive integer indicates
the position of the element to be read.
1, 2, 3, 4, 5 #}.f()
{# where{
f(t) = {#
.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
t;
#}}
{# 1, 3, 6, 10, 15 #}
The selector indices and element types are checked in the type-checking phase so that it is not possible to use an invalid selector.
To support the syntax similar to records, the symbol ‘$
’
can be used instead of the dot. The two syntaxes are equivalent -
x$1
is the same as x.1
.
Tuple updaters are operators that return a copy of a tuple in which one of the tuple elements has been replaced by the given value. They are called updaters, but they do not modify the given tuple - instead they construct a new tuple with the copied and changed values.
Tuple updaters have the following basic syntax:
<tuple>^<n>(<value>)
, where a positive
integer <n>
specifies the position of the element to
be replaced and the <value>
specifies its new
value.
{# 1, 2, 3 #}^1(11),
{# 1, 2, 3 #}^2(22),
{# 1, 2, 3 #}^3(33)
{# #}
{# {# 11, 2, 3 #}, {# 1, 22, 3 #}, {# 1, 2, 33 #} #}
As with the selectors, the indexes and types of the updaters are checked in the type-checking phase, so that it is not possible to use an invalid selector.
The tuple data type is written as follows:
Tuple[<el
1
-type>, <el
2
-type>, ..., <el
n
-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 i
th element of
T1
is the same as the type of the i
th 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.6, 'ab' #}
The types of the .2
selector and the ^2
updater are for example:
(TupleX['1,'2]['3] -> '2)
(TupleX['1,'2]['3] * 2 -> Tuple['1,'2]['3])
where TupleX
stands for extendable tuple and
'3
denotes an optional tuple extension type. This is
necessary for the subtypes to work properly.
On the other hand, each tuple has an exact tuple type. For example,
the result tuple in the last example has the type:
Tuple[ Int, Int, Int, Float, String ]
.
Record is a structured data type. It consists of a series of named values. Record elements can have different types.
Literal records are written in curly brackets { ... }
.
The elements are separated by commas or semicolons. Each element has a
name and a value. Names and values are separated by colons
‘:
’ or the equals sign ‘=
’.
The syntax is flexible in order to be compatible with different languages, but it must be strictly adhered to - it is not permitted to combine different separators in one record.
As with tuples, it is permitted to specify the 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:
{: { a=1 },
fst: { i:1, s:'a', trd:{ nme='list'; lst=[1,2,3] }},
scd// third: "This element is commented out, but previous separator is OK."
}
{ fst: { a: 1 }, scd: { i: 1, s: 'a', trd: { lst: [1, 2, 3], nme: 'list' } } }
Record elements are accessed via record selectors. Record
selectors use names to access the elements. The syntax is similar to
tuple selectors, but the dot cannot be used here, instead
‘$
’ is used: <record>$<name>
,
where <name>
is a record element name.
A selector .<name>
is applicable to any record
that has an element with the specified name.
:1, b:2, c:3, d:4, e:5 }.f()
{ awhere{
f(t) = {#
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
t$a ;
#}}
{# 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.
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
.
{#: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 #}
{# { a: 5, b: 2 }, { a: 1, b: { x: 5, y: 3 } }, { x: 5, y: 3 } #}
The record data type is written as follows:
Record[<name
1
>:<type
1
>, <name
2
>:<type
2
>, ..., <name
n
>:<type
n
>]
.
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.
{#:1, b:2 }.f(),
{ a:1, b:2, a:3 }.f(),
{ q: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()
{ a
#}where{
f(t) = t$a + t$b;
}
{# 3, 5, 3, 3.6, 'ab' #}
If a function or another context allows a record type or its subtype,
the type is usually annotated as follows:
RecordX[name:type]['2]
, where RecordX
stands
for extendable record and '2
represents an
optional record extension.
For example, the types of the $name
selector and the
^name
updater are:
(RecordX[name:'1]['2] -> '1)
(RecordX[name:'1]['2] * '1 -> RecordX[name:'1]['2])
Tuple and record elements can have functional types. The following example uses a collection of named float functions to compute a table with its values.
apply( [
:"Id "; fn:\x:x },
{ name:"Sine "; fn:sin },
{ name:"Cosine "; fn:cos },
{ name:"Square "; fn:\x:x*x }
{ name],
[0.0, 0.5, 1.0, 1.5]
)where{
apply( functions, values ) =
functionsmap(\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.4794255386 0.8414709848 0.9974949866
Cosine 1 0.8775825619 0.5403023059 0.07073720167
Square 0 0.25 1 2.25