Parentheses can be used to group a sequence of instructions into one. In this case, the returned
value is the value of the last instruction :
Parentheses can also be used to explicitly build an expression. In the case of boolean evaluation
(for example in an if), any expression is considered as true except false, empty sets and empty
lists :
(1 + 2) * 3 if (x = 2 & l) |
Local variables can be introduced in a block with the let construct. These variables can be typed,
but it is not mandatory (CLAIRE will use type inference to provide with a reasonable type). On
the other hand, unlike languages such as C++, you always must provide an initialization value
when you define a variable. A let instruction contains a sequence of variable definitions and,
following the in keyword, a body (another instruction). The scope of the local variable is exactly
that body and the value of the let instruction is the value returned by this body.
| let x := 1, y := 3 in (z := x + y, y := 0) |
Notice that CLAIRE uses := to represent assignment and = to represent equality.
The compiler will issue a warning if a statement (x = y) is used where an assignment was
probably meant (this is the case when the value of the assignment is not needed, such as
in x := 1, y = 3, z := 4). The value of local variables can be changed with the same syntax as an update to an object:
the syntax :op is allowed for all operations op :
| x := x + 1, x :+ 1, x :/ 2, x :^ 2 |
The name of a local variable can be any identifier, including the name of an existing object
or variable. In that case, the new variable overrides the older definition within the scope
of the let. While this may prove useful in a few cases, it should be used sparingly since it
yields to code that is hard to read. A rule of thumb is to avoid mixing the name of variables
and the name of properties since it often produces errors that are hard to catch (the property
cannot be accessed any more once a variable with the same name is defined). The control
structure when is a special form of let, which only evaluates the body if the value of the
local variable (unique) is not unknown (otherwise, the returned value is unknown). This is
convenient to use slots that are not necessarily defined as in the following example :
when f := get(father,x) in printf("his father is ~S\n", f) |
The default behavior when the value is unknown can be specified using the else keyword.
The statement following the else keyword will be evaluated and its value will be returned
when the value of the local variable is unknown :
when f := get(father,x) in printf("his father is ~S\n", f) else printf("his father is not known at the present time\n") |
Local variables can also be introduced as a pattern, that is a tuple of variables. In that
case, the initial value must be a tuple of the right length. For instance, one could write :
| let (x, y, z) := tuple(1, 2, 3) in x + y + z |
The tuple of variable is simply introduced as a sequence of variables surrounded by two
parentheses. The most common use of this form is to assign the multiple values returned
by a function with range tuple, as we shall see in the next section. If we suppose that f
is a method that returns a tuple with arity 2, then the two following forms are equivalent:
let (x1,x2) := f() in ...
let l := f(), x1 := l[1], x2 := l[2] in ... |
[XL] In XL CLAIRE, as a syntactical shortcut, we can define in a single let statement both
tuple assigment and normal variable assigment as in :
let (x1,x2) := f(), x3 := g() in ... |
Tuples of variables can also be assigned directly within a block as in the following example :
| (x1, x2) := tuple(x2, x1) |
Although this mostly used for assigning the result of tuple-valued functions without any useless
allocation, it is interesting to note that the previous example will be compiled into a nice
value-exchange interaction without any allocation (the compiler is smart enough to determine
that the list "list(x2,x1)" is not used as such). The key principle of lexical variables is that they are local to the "let" in which they are
defined. CLAIRE supports another similar type of block, which is called a temporary slot
assignment. The idea is to change the value of a slot but only locally, within a given expression.
This is done as follows:
changes the value of r(x) to y, executes e and then restore r(x) to its previous value. It is
strictly equivalent to
let old_v := x.r in (x.r := y, let result := e in (x.r := old_v, result)) |
CLAIRE provides automatic type inference for variables that are defined in a let so that explicit
typing is not necessary in most of the cases. Here are a few rules to help you decide if you need
to add an explicit type to your variable or even cast a special type for the value that is
assigned to the variable :
- (a) Type inference will provide a type to a Let variable only if they do not have one already.
- (b) when you provide a type in let x:t := y, the compiler will check that the value y belong
to t and will issue a warning and/or insert a run-time type-check accordingly.
- (c) if you want to force the type that is inferred to something smaller than what CLAIRE thinks
for y, you must use a cast :
| let x := (y as t2) in ... |
To summarize :
- in most cases CLAIRE range inference works, so you write let x := y in ...
- you use let x:t := y to weaken the type inference, mostly because you want to put
something of a different type later
- you use let x := (y as t) to narrow the type inferred by CLAIRE.