-
Notifications
You must be signed in to change notification settings - Fork 10
Alk by examples
Alk is an algorithmic language intended to be used for teaching data structures and algorithms using an abstraction notation (independent of programming language).
The goal is to have a language that:
-
is simple to be easily understood;
-
is expressive enough to describe a large class of algorithms from various problem domains;
-
is abstract: the algorithm description must make abstraction of implementation details, e.g., the low-level representation of data;
-
is a good mean for learning how to algorithmically think;
-
supply a rigorous computation model suitable to analyse algorithms;
-
is executable: the algorithm can be executed, even if they are partially designed;
-
is accompanied by a set of tools helping to analyse the algorithm correctness and the efficiency;
-
input and output are given as abstract data types, ignoring implementation details.
As a starting example we consider the Alk description of the Euclid algorithm:
gcd(a, b)
{
while (a != b) {
if (a > b) a = a - b;
if (b > a) b = b - a;
}
return a;
}
The algorithm is described using a C++-like notation. The name of the
alghorithm is gcd
and its input parameters are a
and b
. There is
no need to declare the type of parameters and/or the type of the return
value. In order to execute the gcd
algorithm, just add a single line
algorithm
print(gcd(12, 8));
and execute it (“gcd.alk” is the file including the above code):[^1]
> alki -a gcd.alk
4
An alternative is to write a general call of the algorithm
print(gcd(u, v));
and mention the initial values of the global variables u and v in the command line
> alki -a gcd.alk -i "u|->28v|->35"
7
or in an input file, say “gcd.in”:
u |-> 42 v |-> 56
and give it as a parameter of the command line:
> alki -a gcd.alk -i gcd.in
14
A more complex algorithm is the DFS traversal of a digraph represented with external adjacent lists:
dfsRec(D, i, out S) {
if (S[i] == 0) {
// visit i
S[i] = 1;
p = D.a[i];
while (p.size() > 0) {
j = p.topFront();
p.popFront();
dfsRec(D, j, S);
}
}
}
// the calling algorithm
dfs(D, i0) {
i = i0;
while (i < D.n) {
S[i] = 0;
i = i + 1;
}
dfsRec(D, i0, S);
return S;
}
print(dfs(D, i0));
To execute the above algorithm on the digraph:
D.n = 3,
D.a[0] = <1, 2>
D.a[1] = <2, 0>
D.a[2] = <0>
create a file “dfs.in” with the following contents:
D |-> { n -> 3
a -> [ < 1, 2 >, < 2, 0 >, < 0 > ] }
i0 |-> 1
and then execute the algorithm with this input:
> alki -a dfsrec.alk -i dfs.in
[1, 1, 1]
This is a in progress document that is incrementally updated.
The examples used in this manual can be found in the folder “doc/examples-from-manual”.
Alk includes two categories of values: 3ex
Scalars (primitive values). Here are included the booleans, integers, rationals (floats), and strings. *Structured values. Here are included the sequences (linear lists), arrays, structures.
Note that a data can be as complex as possible, i.e, we may have arrays of sequences, arrays of arrays, sequences of arrays of structures, structures of arrays and lists, and so on.
The scalars are written using a syntax similar to that from the most popular programming languages:
index = 234;
isEven = true;
radius = 21.468;
name = "john";
The execution of the above algorithm produces an output as expected:
> alki -a scalars.alk -m
isEven |-> true
name |-> "john"
index |-> 234
radius |-> 21.468
An array value is written as a sequence surrounded by square brackets: [ v0, ... ,v_n-1], where vi is a value, for i=0,...,n-1. Here is a very simple algorithm handling arrays:
Algorithm
a = [3, 5, 6, 4];
i = 1;
x = a[i];
a[i+1] = x;
print(x);
print(a);
Output
> alki -a arrays.alk
5
[3, 5, 5, 4]
The multi-dimensional arrays are represented a arrays of arrays:
a = [ [ 1, 2, 3 ], [ 4, 5, 6 ] ];
b = a[1];
c = a[1][2];
a[0] = b;
a[1][1] = 89;
print(a); // [[4, 5, 6], [4, 89, 6]]
w = [ [ [ 1, 2], [ 3, 4 ] ], [ [ 5, 6 ], [ 7, 8 ] ] ];
x = w[1];
y = w[1][0];
z = w[1][0][1];
w[0][1][0] = 99;
print(x); // [[5, 6], [7, 8]]
print(y); // [5, 6]
print(z); // 6
print(w); // [[[1, 2], [99, 4]], [[5, 6], [7, 8]]]
The output is indeed the expected one:
> alki -a arraysofarrays.alk
[[4, 5, 6], [4, 89, 6]]
[[5, 6], [7, 8]]
[5, 6]
6
[[[1, 2], [99, 4]], [[5, 6], [7, 8]]]
A sequence value is written in a similar to an array, but using angle brackets: < v0,...,vn-1 >, where vi is a value, for i=0,...,n-1. The list of operations over sequences includes:
Method | Behaviour |
---|---|
emptyList() |
returns the empty list < > |
L.topFront() |
returns v0 |
L.topBack() |
returns vn-1 |
L.at(i) |
returns vi |
L.insert(i,x) |
returns <...vi-1,x,vi,...> |
L.removeAt(i) |
returns <...vi-1,vi+1,...> |
L.removeAllEqTo(x) |
returns L, where all elements vi equal to x were removed |
L.size() |
returns n |
L.popFront() |
returns < v1,...,vn-1 > |
L.popBack() |
returns < v0,...,vn-2 > |
L.pushFront(x) |
returns < x, v-,...,vn-1 > |
L.pushBack(x) |
returns < v0,...,vn-1, x > |
L.update(i,x) |
returns < ...vi-1,x,vi+1 > |
Algorithm: ll = < 8, 3, 9, 4, 5, 4 >; i = 1; x = ll.at(i + 1); y = ll.topFront(); print(x); // 9 print(y); // 8 ll.insert(2, 22); ll.update(3, 33); print(ll); // < 8, 3, 22, 33, 4, 5, 4 > l2 = ll; l2.removeAt(0); l2.removeAt(3); print(l2); // < 3, 22, 33, 5, 4 > l2.removeAllEqTo(4); print(l2); // < 3, 22, 33, 5 >
Output:
> alki -a seq.alk
9
8
[8, 3, 22, 33, 4, 5, 4]
[3, 22, 33, 5, 4]
[3, 22, 33, 5]
Now we may define sequences of arrays:
l = < [1, 2, 3], [4, 5] >;
a = l.at(1);
l.pushBack(a);
print(a); // [4, 5]
print(l); // <[1, 2, 3], [4, 5], [4, 5]>
Output:
> alki -a sequencesofarrays.alk
[ 4, 5 ]
< [ 1, 2, 3 ], [ 4, 5 ], [ 4, 5 ] >
and arrays of structures:
a = [ { x -> 1 y -> 2 }, { x -> 4 y -> 5 } ];
b = a[1];
c = a[1].y;
a[1].x = 77;
print(a); // [{x -> 1, y -> 2}, {x -> 77, y -> 5}]
print(b); // {x -> 4, y -> 5}
print(c); // 5
Output:
> alki -a arraysofstructures.alk
[{x -> 1, y -> 2}, {x -> 77, y -> 5}]
{x -> 4, y -> 5}
5
A structure value is of the form { f1 -> v1 ... fn -> vn }, where fi is a field name and vi is a value, for i=1,...,n.
Example:
s = { x -> 12 y -> 45 };
a = s.x;
s.y = 99;
b.x = 22;
print(s); // {x -> 12, y -> 99}
print(b); // {x -> 22}
Output:
> alki -a structures.alk
{ (x -> 12) (y -> 99) }
{ x -> 22 }
Note that the structure b has been created with only one field, because there is no information about its type, which is deduced on the fly during the execution.
We may have structures of arrays:
s = { x -> [ 1, 2, 3 ] y -> [ 4, 5, 6 ] };
b = s.y;
s.x[1] = 11;
print(b); // [4, 5, 6]
print(s); // {x -> [1, 11, 3], y -> [4, 5, 6]}
Output:
> alki -a structuresofarrays.alk
[ 4, 5, 6 ]
{ (x -> ([ 1, 11, 3 ])) (y -> [ 4, 5, 6 ]) }
sequences of structures:
l = < { x -> 12 y -> 56 }, { x -> -43 y -> 98 }, { x -> 33 y -> 66 } >;
u = l.topFront();
l.pushBack({ x -> -100 y -> 200 });
print(u);
print(l);
Output:
> alki -a seqofstructures.alk
{x -> 12, y -> 56}
<{x -> 12, y -> 56}, {x -> -43, y -> 98}, {x -> 33, y -> 66}, {x -> -100, y -> 200}>
and so on.
A set value is written as { v0,...,vn-1 }$, where vi is a value, for i=0,...,n-1. The operations over sets include the union U, the intersection ^, the difference , and the membership test in. Example:
Algorithm:
s1 = { 1 .. 5 };
s2 = { 2, 4, 6, 7 };
a = s1 U s2 ;
b = s1 ^ s2;
c = s1 \ s2;
print(a); // {1, 2, 3, 4, 5, 6, 7}
print(b); // {2, 4}
print(c); // {1, 3, 5}
t = 2 in b ^ c;
print(t); // false
x = 0;
foreach y from s2 x = x + y;
print(x); // 19
d = emptySet;
foreach y from { 1 .. 6 }
if (y in s2) d = d U singletonSet(y);
print(d); // {7}
Output:
> alki -a sets.alk
{1, 2, 3, 4, 5, 6, 7}
{2, 4}
{1, 3, 5}
false
19
{2, 4, 6}
Obviously, we may have sets of arrays, sequences of sets, and so on.
The current implementation does check if a set value assigned to a variable is indeed a set. But the operations returns sets whenever the arguments are sets.
Alk includes several sugar syntax mechanisms for specifying values in a more compact way:
Algorithm:
p = 3;
q = 9;
a = [ i | i from [p .. q] ];
p = 2;
b = [ a[i] | i from [p .. p+3] ];
l = < b[i] * 2 | i from [p-2 .. p] >;
print(a);
print(b);
print(l);
Output:
> alki -a specs.alk
[3, 4, 5, 6, 7, 8, 9]
[5, 6, 7, 8]
<10, 12, 14>
Alk includes the basic operators over scalars with a C++-like syntax.
Since Alk is designed with K Framework, it can be easily extended with new operators.
The syntax for the statements is similar to that of imperative C++.
We already have seen examples of the assignment statement. The other statements include:
Syntax: {
Stmt }
Syntax: 1) if
(
Exp )
Stmt else
Stmt2) if
(
Exp )
Stmt
Syntax: while (
Exp )
Stmt
Syntax: 1) foreach
Id from
Exp
Stmt 2) for
(
VarAssign ;
Exp ;
VarUpdate )
Stmt
Examples:
for (i= 2; i <= x / 2; ++i)
if (x % i == 0) return false;
foreach y from { 1 .. 6 }
if (y in s2) d = d U singletonSet(y);
Syntax: Stmt Stmt
Syntax: choose
Id from
Exp ;
2)
choose
Id in
Exp s.t.
Exp ;
Example 1:
choose x1 from { 1 .. 5 };
choose x2 from { 1 .. 5 };
Output:
x1 |-> 3
x2 |-> 1
Example 2:
odd(x) {
return x % 2 == 1;
}
choose x1 from { 1 .. 5 } s.t. odd(x1);
choose x2 from { 1 .. 5 } s.t. odd(x2);
Output:
x1 |-> 5
x2 |-> 3
Example 3:
choose x8 from { 1 .. 5 } s.t. x8 > 6;
Output:
Error at line 14: Choose can't find any
suitable value.
Syntax: success
;
Example:
odd(x) {
return x % 2 == 1;
}
choose x from { 1 .. 8 };
if (odd(x)) success;
Output:
> alki -a success.alk
success
x |-> 5
Syntax: failure
;
Example:
odd(x) {
return x % 2 == 1;
}
choose x from { 1 .. 8 };
if (odd(x)) success;
else failure;
Output:
> alki -a success.alk
failure
x |-> 8
Example:
swap(out a, i, j) {
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
partition(out a, p, q) {
x = a[p] ;
i = p + 1; j = q;
while (i <= j) {
if (a[i] <= x) i = i+1;
else if (a[j] >= x) j = j-1;
else if (a[i] > x && x > a[j]) {
swap(a, i, j);
i = i+1;
j = j-1;
}
}
k = i-1; a[p] = a[k]; a[k] = x;
// if (k == q) --k;
return k;
}
qsort(out a, p, q) {
if (p < q) {
k = partition(a, p, q);
qsort(a, p, k-1);
qsort(a, k+1, q);
}
}
b = [5,1,3,2,4];
n = 5;
qsort(b, 0, n-1);
print(b);
Output:
> alki -a qsort.alk
[1, 2, 3, 4, 5]
Note that the output parameters and the input/output parameters are declared with the prefix out.
If a function modifies global variables, then these must be specified in a “modifies” clause. Example:
x = 3;
y = 5;
g(b) modifies x, y {
x = x + b;
y = y * b;
return x;
}
g(5);
print(x); // 8
print(y); // 25
Output:
> alki -a globals.alk
8
25
[^1]: In this document “alki” denotes one of the two scripts running the Alk interpretes: “alki.bat” (for Windows platform), respectively “alki.sh” (for linux, Mac OS).