The extensions listed here are mostly syntactic sugar that is also expressible by other, less convenient means:
The comment marker (*) introduces a comment that stretches to the end of line:
fun myf x = bla (*) my function fun myg x = blo (*) my second function
Line comments properly nest into conventional comments, so the following is one single comment, even though the inner line comment contains a closing comment delimiter:
(* fun myf x = bla (*) my function *) *)
Numeric literals may contain underscores to group digits:
val pi = 3.141_592_653_596 val billion = 1_000_000_000 val nibbles = 0wx_f300_4588
Moreover, binary integer and word literals are supported:
val ten = 0b1010 val bits = 0wb1101_0010_1111_0010
Long identifiers are not part of the lexical syntax, but of the context-free grammar. Consequently, there may be arbitrary white space separating the dots from the identifiers:
mod . submod (*Here!*) . subsub . f
For debugging purposes, Alice supports two special expression derived forms:
_file_ _line_
Occurrences of these keywords are replaced by the file name and the line number of their respective occurrence in the source file. For example,
print ("Error in file " ^ _file_ ", line " ^ Int.toString _line_ ^ "\n")
Following SML/NJ, Alice provides vector expressions and patterns:
val v = #[1, 2, 4, 1, 2] fun f #[] = 0 | f #[n] = n | f v = 1
atexp | ::= | ... | |
#[ exp1 , ... , expn ] | vector (n≥0) | ||
atpat | ::= | ... | |
#[ pat1 , ... , patn ] | vector (n≥0) |
While SML allows punning in record patterns (so that the left hand side of the former example is legal), it does not allow punning in record expressions. In Alice, the latter is also available as a simple derived form, dualing the derived form for patterns. For example, the declaration
fun f {a,b,c} = {a,b}
is understood as an abbreviation for
fun f {a = a, b = b, c = c} = {a = a, b = b}
The labels may have type annotations, i.e. {a : int, b} abbreviates {a = a : int, b = b}.
Ellipses in a record pattern may be followed by a pattern that will be matched against the "remaining" record. For example,
val r = {a = 1, b = true, c = "hello"} val {a = n, ... = r'} = r
will bind r' to the record {b=true, c="hello"}.
Analogously, records can be constructed as extension of other records using ellipses:
val r'' = {d = 3.14, ... = r'}
This example will bind r'' to the record {b=true, c="hello", d=3.14}. Note that the ellipses expression must have record type, but this record may not contain any of the added labels.
Record extension can also be used in type expressions:
type 'a t = {a : 'a, b : bool} type u = {c : char, d : string, ... : int t}
Now the type u is equivalent to {a:int, b:bool, c:char, d:string}. Again, the label sets of the ellipses type and the explicit fields must be disjoint.
In all three forms, the ellipses may appear anywhere in the row, but a row may not have multiple occurences of ellipses.
There also is syntax for functional record update. For example,
let val r = {a = 1, b = true, c = "hello"} in {r where a = 3, c = "bye"} end
evaluates to
{a = 3, b = true, c = "bye"}
Here, the updated record must have a type containing all labels being updated. However, the types of these fields may change with the update.
atexp | ::= | ... | |
{ atexp where exprow } | record update | ||
exprow | ::= | ... | |
vid <: ty> <, exprow> | label as expression | ||
... = exp <, exprow> | ellipses | ||
patrow | ::= | ... | |
... <= pat> <, patrow> | ellipses | ||
tyrow | ::= | ... | |
... : ty <, tyrow> | ellipses |
Labels as expressions are a derived form. Likewise is record update:
vid <: ty> <, exprow> | ==> | vid = vid <: ty> <, exprow> |
{atexp where exprow} | ==> | let val {patrow, ... = vid} = atexp in {exprow, ... = vid} end |
In the rewritten form of record update, vid is a fresh identifier, and patrow is obtained from exprow by replacing all right-hand sides with wildcards.
Alternative patterns (also called or patterns) are present in SML/NJ as well and allow more compact case analysis:
fun fac (0|1) = 1 | fac n = n * fac(n-1)
The syntax subsumes multiple or-patterns and multiple matches:
case exp of | A | B | C => 1 | D | E => 2
The patterns nested inside an alternative pattern may bind variables, but all patterns must bind exactly the same set of variables with the same type.
Layered patterns (also called as patterns) have been generalized to allow arbitrary patterns on both sides (in contrast to just an identifier on the left hand side as in SML). This is useful as it allows to put the identifier on either side:
fun f(xs as x::xr) = bla fun g(x::xr as xs) = blo
The most important extension are pattern guards. These allow decorating patterns with boolean conditions. A guarded pattern matches, if the pattern matches and the guard expression evaluates to true. The guard expression can refer to variables bound by the nested pattern:
fun f(x,y) if (x = y) = g x | f(x,y) = h y
Guards can be nested into other patterns. Any side effect produced by the guard expression occurs whenever the pattern is tried to be matched.
atpat | ::= | ... | |
( pat1 | ... | patn ) | alternative (n≥2) | ||
pat | ::= | ... | |
pat as pat | layered (R) | ||
pat if atexp | guarded (L) | ||
exp | ::= | ... | |
exp handle <|> match | handle exception | ||
case exp of <|> match | case analysis | ||
fn <|> match | function | ||
datbind | ::= | tyvarseq tycon = <|> conbind <and datbind> | |
datdesc | ::= | tyvarseq tycon = <|> condesc <and datdesc> |
An optional bar is allowed before the first rule of a match, yielding more regular and editing-friendly notation:
case f n of | LESS => f(n-1) | EQUAL => n | GREATER => f(n+1)
This is also supported for fn and handle, and for function declarations using fun.
Optional bars are also allowed with datatype declarations and specifications:
datatype order = | LESS | EQUAL | GREATER
In a similar vein, a terminating semicolon optionally is allowed in sequence expressions:
( print "["; print(Int.toString n); print "]"; )
and
let val s = Int.toString n in print "["; print s; print "]"; end
SML only allows function expressions on the right hand side of val rec. Alice is a bit more permissive. For example, one can construct cyclic lists:
val rec xs = 1::2::xs
Or regular trees:
datatype tree = LEAF | BRANCH of tree * tree val rec tree = BRANCH(BRANCH(tree,LEAF), tree)
The right-hand sides of recursive bindings may be any expressions that are non-expansive (i.e. syntactic values) and match the corresponding left-hand side patterns (statically). Unfounded recursion is legal, but evaluates to futures that cannot be eliminated:
val rec (x,y) = (y,x)
Note that the same data structures are constructable by explicit use of promises.
Recursive values may be constructed directly without resorting to recursive declarations:
val l = rec xs => 1::2::xs
exp | ::= | ... | |
rec pat => exp | recursion |
Such rec expressions are expanded as a derived form:
rec pat => exp | ==> | let val rec vid as pat = exp in vid end |
where vid is a new identifier.
For imperative code it is sometimes convenient to have a conditional without an obligatory else branch. Alice ML provides this as a trivial derived form.
exp | ::= | ... | |
if exp1 then exp2 <else exp3> | conditional |
A conditional with an omitted branch is a derived form:
if exp1 then exp2 | ==> | if exp1 then exp2 else () |
Note that the expansion implies that exp2 must have type unit.
Cleaning up resources after a computation usually has to be done even if the computation exits abnormally, with an exception. To make this convenient, Alice ML has syntactic sugar for performing a finalization action after evaluating another expression, regardless of whether this other expression terminates regularly or exceptionally.
exp | ::= | ... | |
exp1 finally exp2 | finalization (L) |
Finalization is a derived form:
exp1 finally exp2 | ==> | let val vid2 = fn _ => exp2 val vid1 = exp1 handle vid3 => (vid2(); raise vid3) in vid2(); vid1 end |
where vid1,...,vid3 are new identifiers.
Note that finally can be conveniently combined with handle, to achieve an effect similar to the try...catch...finally syntax known from other languages, e.g.:
exp0 handle p1 => exp1 | p2 => exp2 finally exp3
Syntactic sugar is provided for expressing preconditions and other assertions conveniently. If an assertion fails, the exception Assert is raised, indicating the location of the failed assertion. Assertions can be activated selectively with the --assert compiler switch.
exp | ::= | ... | |
assert<d> exp | boolean assertion | ||
assert<d> exp of pat | pattern assertion | ||
assert<d> exp raise pat | exception assertion | ||
assert<d> exp do exp | boolean precondition | ||
assert<d> exp of pat do exp | pattern precondition |
Assertions are derived forms:
assert exp1 do exp2 | ==> | if exp1 then exp2 else raise Assert(_file_,_line_) |
assert exp1 of pat do exp2 | ==> | assert (case exp1 of pat => true | _ => false) do exp2 |
assert exp | ==> | assert exp do lazy raise Assert(_file_,_line_) |
assert exp of pat | ==> | assert exp of pat do () |
assert exp raise pat | ==> | assert ((exp; false) handle pat => true | _ => false) do () |
Here, _line_ will be on the same line where the assert keyword occurred.
Assertions can be assigned an explicit level by appending a digit to the assert keyword. Such assertions can be controlled via the --assert compiler switch that specifies the assertion level (0-9) of the compiled code. Any assertion that has a level annotation higher then that assertion level will be disabled. Note that level 0 assertions cannot be disabled. An unannotated assertion defaults to level 0.
When an assertion is disabled, the basic derived form is resolved differently:
assertd exp1 do exp2 | ==> | exp2 |
From this, the other expansions follow:
assertd exp1 of pat do exp2 | ==> | exp2 |
assertd exp | ==> | lazy raise Assert(_file_,_line_) |
assertd exp of pat | ==> | () |
assertd exp raise pat | ==> | () |
Note that boolean assertions of the form assert exp are fully polymorphic. This enables the following idiom for indicating "impossible" cases:
fun f (x::xs) = g x | f nil = assert false (* we know the list is non-empty *)
The value of such an assert expression should never be used and is thus represented by a failed future. Accessing it will also raise an Assert exception.
The common SML idiom of writing a declaration
val _ = exp
for causing a side effect can be abbreviated to
do exp
This is a simple derived form:
do exp | ==> | val _ : {} = exp |
I.e. the expression must have type unit.