Encore Syntax
This section describes the syntactic and semantics of various aspects of Encore.
Blocks and Delimiters
The basic delimiter in Encore is a linebreak (newline). Blocks are
keyword-based, and in most cases end with an end
keyword.
With the exception of if
and let
expressions, a newline is mandatory
to open a new block, so after do
and with
, the newline is
required:
while Expr do for Id <- Expr do match Expr with
... ... case Pattern => Expr
end end end
let let Id = Expr in repeat Id <- Expr do
Id = Expr ... ...
in end end
...
end
These "in-line forms" are allowed (single line, no line breaks):
let Id = Expr in ... end
if Expr then Expr end
if Expr then Expr else Expr end
Encore also allows one-line blocks on the form { Expr (; Expr)* }
.
Expressions and Statements
Encore is an expression-based language, meaning that many constructs that would be expressions in many mainstream imperative languages are expressions. For example:
Block
(e.g.,do
...end
) returns the value of the last expression in the body/blockif Expr then Block1 else Block2 end
returns the value of the evaluated blockif Expr then Block end
returnsNothing
ifExpr
is false, else it returnsJust(v)
wherev
is the return value ofBlock
(cf. maybe types below)x = Expr
returns()
which is the "unit value"
While assignments are expressions, variable declarations are not.
Note that since the return value of a method body is the last
expression in the body, explicit return
is mostly useful to
return from the middle of the body. The following two methods are
equivalent:
fun id[t](x:t) : t
x
end
fun id[t](x:t) : t
return x
end
Note that return
is not allowed from inside a closure.
Maybe types and the absence of NULL
Encore does not have a NULL type. Instead, Encore relies on a
maybe type. There are two possible values in a type Maybe[t]
:
Just(v)
wherev
is a value of typet
Nothing
which is a value denoting the absence of at
-typed value (in this case)
Where a C or Java programmer might use NULL
or null
, the
Encore programmer will use Nothing
. The key advantage of maybe
types is that they capture precisely where absences of values may
occur in practise, and in that respect they are similar to
non-null types of some imperative languages, where non-null is the
default for a type (e.g., t
) and must be opted in (i.e.,
Maybe[t]
).
The maybe type is immutable and has value semantics and thus may be passed by copy.
How to extract v
from a maybe value is covered below under
conditionals.
Conditionals
Examples:
if foo < bar then
print("Foo is less than bar")
else
print("Bar is less than or equal to foo")
end
if foo < bar then
print("Foo is less than bar")
else if foo > bar then
print("Bar is less than foo")
else
print("Foo and bar are equal")
end
Encore support three basic forms of conditionals:
if
...then
...end
if
...then
...else
...end
Conditionals branch on expressions of boolean types and Maybe
types. In the latter case, Just(_)
coerces to true and
Nothing
coerces to false.
Here is an example of branching on a Maybe type, returned by the
to_i()
method:
let
x = "42".to_i
in
if x then
print("String was successfully converted to an int")
else
print("String could not be converted to an int")
end
end
Typing Conditionals
Typing of conditionals is straightforward. Note that a conditional is an expression and the type of a conditional is the union type of the blocks:
Expr1 : bool \/ Expr1 : Maybe[T1]
Expr2 : T2
Expr3 : T3
T = T2 & T3
---------------------------------------
if Expr1 then Expr2 else Expr3 end : T
This means that a conditional whose branches both return the type
T
will have the type T
itself. Thus, the following is well-typed:
var tmp : int = if coin_toss() then 42 else 4711 end
However, the following is not:
var tmp : int = if coin_toss() then 42 else "42" end
because the type int & String
does not equal int
. Read more
about union types in its section of the Encore manual.
Conditionals that lack an else-branch have a Maybe type,
where taking the else-branch returns Nothing
:
Expr1 : bool \/ Expr1 : Maybe[T1]
Expr2 : T2
---------------------------------------
if Expr1 then Expr2 end : Maybe T2
This can be explained as the following transformation:
if Expr1 then Expr2 end
is translated into
if Expr1 then Just(Expr2) else Nothing end
Pattern matching
Examples:
val v = lookup_person(social_sec_number)
match v with
case Just(person) => p.name
case _ => "No person found!"
end
val s = get_shape()
match s with
case Circle(radius) => ...
case Rectangle(Point(x1,y1), Point(x2,y2)) => ...
end
Encore supports pattern matching on objects and values. A pattern match expression has the following syntactical form:
match Expr with
case Pattern1 => Expr -- for simple patterns
case Pattern2 do -- blocks for complicated patterns
...
end
case ...
end
Pattern are tested in the order they appear, so Pattern1
will
be tested before Pattern2
.
A match expression that does not match on a pattern results in a run-time error.
The expression Expr
can be of any type, including objects,
primitives, tuples and Maybe types. For example, the example above
branching on a Maybe in a conditional is equivalent to the
following code:
let
x = "42".to_i
in
match x with
case 42 => print("{} can be converted to a number\n", x)
otherwise => print("{} can not be converted to a number\n", x)
end
end
Patterns can have any of the following form:
- Binders (e.g., variables like
x
) - Primitive value (e.g.,
1
,42.0
,false
) - Literal patterns like tuples, arrays, strings and maybes (e.g.,
(42, x)
,[1,2,x]
,"foo"
, andJust(42)
) - Extractor patterns, which involve calling extractor methods (see below)
Binders are free variables which match against anything and are
bound in the corresponding block. Primitive value patterns use
equality comparison. Literal patterns can be reduced to a
structural match, followed by zero or more primitive value
comparisons and/or binders. Thus, the following match will return
3
because the array pattern matches the array v
as they have
the same number of elements, and the point-wise comparison of each
element matches: 1
is 1
, 2
is 2
and z
matches is bound
to 3
. Hence the entire match returns 3
.
val v = (1,2,3)
match v with
case (1,2,z) => z
case ...
...
end
Tuple, maybe and string patterns are tested according to similar principles: the elements of the structures are matched against each other.
In Encore objects may define extractor methods which allows
running arbitrary, side-effect free code as part of a pattern
matching expression. To designate a method as an extractor, add a
match
modifier to the method declaration. Match methods must
return values of Maybe
type. Extractor methods allow an object
to define arbitrary patterns which can be used to extract values.
Consider this example:
class Point
var x : int
var y : int
def match origo() : Maybe[unit]
if this.x == 0 && this.y == 0 then
Just(())
end
end
def match Point() : Maybe[(int, int)]
Just(this.x, this.y)
end
end
The methods origo()
and Point()
are extractor methods, which
can be used as patterns in a match expression:
fun example(p:Point) : unit
match p with
case origo() => println("Point has coordinates (0,0)")
case Point(47,11) => println("Point has coordinates (47,11)")
case Point(42,y) => println("Point has coordinates (42,{})", y)
case Point(a, b) => println("Point has coordinates ({}, {})", a, b)
end
end
The first pattern, origo()
will match when p.x == 0 && p.y == 0
holds.
The second pattern demonstrates patterns with values, and will only match
when p.x == 47 and p.y == 11
. The third pattern matches against any value
of p.y
and binds the pattern variable y
to p.y
in the corresponding
block. Note that it also requires that the value of p.x == 42
.
Finally, the fourth pattern matches any value on p.x
and p.y
and binds
the pattern variables a
and b
to p.x
and p.y
respectively in the
corresponding block.
The return type of match expressions works the same as conditionals, i.e., it is the union type of all return types of all case blocks.
Note that where
clauses can be used to reduce clutter when cases in a
match statement contains many statements:
fun example(p:Point) : unit
match p with
case Pattern1 => ...
case Pattern2 do
...
end
case ...
end
end
can be written as:
fun example(p:Point) : unit
match p with
case Pattern1 => foo()
case Pattern2 => bar()
...
end
where
fun foo() => ...
fun bar()
...
end
end
Encore allows calling match
methods just like regular, and
pattern matching on any method that returns a maybe type, not just
methods explicitly declared as match
. However, code that uses
matches extremely rarely expects side-effects or "long-running
patterns". Pattern matching on non-match
methods should be
avoided, and all methods that are intended for use in pattern
matching should be explicitly designated as match
methods.
Patterns and Shadowing
Note that variables in a pattern are always binders, meaning they
always match against any value (modulo guards), and extract it.
Thus, the following code always extracts the x and y coordinates
of the point p
:
match p with
case Point(x, y) => ...
end
It is not possible to change this behaviour by declaring x
and
y
in the surrounding scope. The following code will not change
the match expression to only match points with x coordinate of 42
and y coordinate of 7:
let
x = 42
y = 7
in
match p with
case Point(x, y) => ...
end
...
To match against a specific point there are two possibilities shown in the two cases below:
let
x = 42
y = 7
in
match p with
case Point(42, 7) => ...
case Point(x', y') when x == x' && y == y' => ...
end
...
Identity or Equality in Matches
As variables only appear as binders in patterns, the programmer has to explicitly make the choice between identity and equality comparisons. This compares identity (and therefore never matches):
match new Object() with
case x when x == new Object() => 1
_ => 0
end
This compares equality and thus may match depending on how equality
is defined for Object
:
match new Object() with
case x when x.eq(new Object()) => 1
_ => 0
end
Typing Match Expressions
Similar to an if-expression, the type of a match is the union of
the return types of the cases, e.g., in the following, the type is
T1 & T2 & T3
:
match foo with
case 1 => T1()
case 2 => T2()
case 3 => T3()
end
Since match expressions that do not match result in an error, the return types is not wrapped in a maybe type, like an if without an else.
Encore tries very hard to hide union types from the programmer, and so union types will currently fail a program unless there is a common interface in the types, which will only be the case when the participating types are composition of traits. Please see the documentation on traits.
While and Do/While Loops
Examples:
-- calculate 10!
var sum = 1
var N = 10
while N > 0 do
sum = sum * N
N = N - 1
end
Encore's while loops follow standard conventions:
while Expr do
...
end
and the do/while version:
do
...
while Expr
The break
and continue
keywords work in the tradition of C
(except that there are no labels), meaning break
exits the
current loop and continue
immediately skips to the next
iteration. Thus, the following loop will only run 10 iterations:
var i = 0
while true do
if i < 10 then
i += 1
else
break
end
end
Similarly, this loop will print all odd numbers:
var i = 0
while i < 1024 do
if i % 2 == 0 then
continue
end
print(i)
end
Except for for
comprehensions (see below), loops do not have a
return value.
For Comprehensions
For comprehensions have not yet been integrated into the main branch.
Examples:
for x <- [1, 2, 3, 4] do
print(x)
end
The Encore for
comprehension is convenience syntax that
logically transforms into more verbose statements at compile-time.
The example above, for instance, is logically transformed into
[1, 2, 3, 4].foreach(fun (x : int) print(x) end)
For comprehensions can equally be used to produce collections of values:
list = for x <- [1, 2, 3, 4] do
x * x
end
is logically transformed into
list = [1, 2, 3, 4].map(lambda x do x * end)
For comprehensions may involve several sources of values, i.e.,
for
x <- [1, 2, 3]
y <- [4, 5, 6]
do
x + y
end
which is almost the same as nested loops, i.e.,
for x <- [1, 2, 3] do
for y <- [4, 5, 6] do
x + y
end
end
The difference between the first and the second is that the first
returns [5, 6, 7, 6, 7, 8, 7, 8, 9]
and the second returns
[[5, 6, 7], [6, 7, 8], [7, 8, 9]]
.
For comprehensions can be used to extract values from maybe types,
e.g., if m
and n
have type Maybe[int]
below, the for
comprehension will produce the sum of their values, else it will
not run.
for
a <- m
b <- n
do
a + b -- a and b have type int here
end
Notably, the entire for comprehension returns a Maybe[int]
as
it is not guaranteed to run -- if either m
or n
holds Nothing,
the value of the entire expression is Nothing.
The binders in for comprehensions are pattern matching, meaning it is possible to filter out certain results:
var sum = 0
for
Person(_, age) <- vs
do
sum += age
end
The code above will iterate over all values of vs
and for
each object that matches the pattern Person(_, age)
, it
will execute the do ... end
block with age
bound to the
age of the current person. It is possible to further narrow
down filtering, e.g.:
Person(_, age) where age > 18 <- vs
To iterate over a collection with index, rely on the standard
zip_with()
in the Iterable
interface:
for
(i, v) <- values.zip_with(InfiniteRange())
do
...
end
or its convenience method with_index()
:
for
(i, v) <- values.with_index()
do
...
end
The with_index()
method takes 2 parameters -- start index and
step, whose default values are 0 and 1 respectively. So does the
constructor of InfiniteRange()
.
Asynchronous For Comprehensions
Example:
for
n1 <- graph1 ! find_smallest()
n2 <- graph2 ! find_largest()
do
n1.size() == n2.size()
end
Asynchronous for comprehensions, such as the one above, only
involve source producing future values. Asynchronous for
comprehensions are lifted into asynchronous blocks, meaning they
are not part of the control flow of the method where they are
specified. The return value of the for comprehension above is a
Future[bool]
.
See also: attached and detached closures.
Binary Operators
Encore supports the following binary operators:
==
-- equality comparison (a -> b -> bool
)&&
-- boolean conjunction (bool -> bool -> bool
)||
-- boolean disjunction (bool -> bool -> bool
)xor
-- exclusive or (bool -> bool -> bool
)!=
-- negated==
%
-- modulo (int -> int -> int
)/
-- integer division (int -> int -> int
)+
-- addition (num -> num -> num
)-
-- subtraction (num -> num -> num
)*
-- multiplication (num -> num -> num
)
where num
ranges over int
and float
types.
Note that identity comparison ==
is only allowed on types that
implement the Id
interface, plus, primitive types, maybe types
and tuples.
In addition to those above, Encore supports the following convenience operators, from the C-family of languages:
+=
-=
*=
/=
Examples:
foo += bar -- equivalent to foo = foo + bar
There are no ++
or --
operations.
Operator Precedence
Below is a full table of operators in precedence order.
- (unary minus)
* / %
+ -
< >
<= =>
== !=
not
&& ||
a(32) a(32) = 32
!
: Type
~~>
=
+= -= *= /=
NOTE: The table above does not contain Par T types.
Short-Circuiting Boolean Operators
The &&
and ||
operators are short-circuiting in the style of
C and Java. This means that when evaluating Expr1 && Expr2
, if
Expr1
is found to return false
, Expr2
will not be evaluated,
since there is no way in which the entire expression can become
true
. Similarly, Expr1 || Expr2
will not evaluate Expr2
if
Expr1
evaluates to true
since there is no way the entire
expression can become false
.
Local variables and Fields
Examples:
val constant = 3.14
var indiana_pi : float = 3
let
x = 100 -- x and y implicitly val
y = x * 2
in
new Point(x, y)
end
Local variables are declared using the val
or var
keywords,
indicating whether the variable is constant (aka final
) or not.
This is the generalised syntax for local variables:
modifier Id [: Type] = Expr
where modifier
is either val
or var
.
In addition, a let
expression allows declaring many local
constants for one block.
Shadowing of local variable names is not allowed, however, a
local variable name can have the same name as a local field,
since fields are always accessed through this
.
Types of variable declarations are inferred, but types may be explicitly stated as exemplified above. Variables must always be initialised.
Fields
This is the generalised syntax for fields (instance variables):
modifier Id [: Type] = Expr
where modifier
is either val
, var
, once
or spec
.
The once
and spec
modifiers are special, and are used
in lock-free programming to specify that a field is a
possible point of contention in a data structure, and therefore
must be written using special rules, see section Type System
for more details
Calling Methods and Sending Messages
Method calls and message sends in Encore always have a receiver. Logically, method calls are synchronous whereas message sends are asynchronous. Syntactically, method calls have the form:
Target . method_name( Arguments )
where Target can be an arbitrary expression and Arguments is a comma-separated list of zero or more expressions. Note that the receiver must always be explicit. Simply writing
method_name( Arguments )
is an error, unless there is a function in the current scope
with that name. Functions can be declared at the top-level of a
module, inside a class, or introduced locally for a method using a
where
-clause.
Message sends have the same syntax as method calls, except they
use !
instead of .
as the operator:
Target ! message_name( Payload )
where Payload is a comma-separated list of zero or more expressions. Message sends are only allowed if the receiver is an actor.
Note that there are strict rules on payloads with respect to how objects may be shared across actors. This is covered in the section about actors.
Finally, as a convenience, there is a ?.
operator for performing
method calls on targets of maybe type.
Target ?. method_name( Arguments )
which is the same as
match Target with
case Just x => x.method_name( Arguments )
case Nothing => pass
end
And an equivalent ?!
operator for message sends.
Literals
Tuples
Example:
val person = ("Bob", 42) -- create tuple with 2 elements
var name = person.0 -- extract first element
Tuples are immutable collections of size 2 or higher of values of
any type. The syntax for tuples is ( Exprs )
where Exprs
is a
comma-separated list of expressions. Tuples have value semantics,
but may contain values with reference semantics. The following code
snippet illustrates that while a
and b
denote two different
copies of the tuple (42, new Person("Bob"))
, identity comparison
of the second element of both tuples holds. It also illustrates
the creation of tuples and extracting of elements from tuples.
let
a = (42, new Person("Bob"))
b = a
in
a.0 == b.0 -- returns true
Statically, a tuple's complete type, including its size, must always be known. Accesses to non-existing elements of tuples will be caught at compile-time. It is not possible to loop over the elements in a tuple.
Arrays
Multi-dimensional array support is not available in the main Encore branch.
Arrays are created with new [t](size)
Example of creation:
-- create 1-d array with 100 elements of type t
val arr1 = Array1[t](10 * 10)
-- create 2-d array with 10x10 elements of type t
val arr2 = Array2[t](10, 10)
-- create 1-d array with 10 elements of type Array1[t]
val arr3 = Array1[Array1[t]](10)
Array literals:
-- create array of type Array1[int]
val arr4 = [1, 2, 3, 4]
Example of manipulation:
-- update 5th element with contents of variable inkal
arr1(4) = inkal
-- update foo and bar with element 3x4 of array
foo = arr2(2, 3)
bar = arr3(2, 3)
Arrays have a fixed size which can not be changed after creation.
The standard library types Array1
and Array2
implement
one-dimensional and two-dimensional arrays, respectively. The
difference between Array1[Array1[t]]
and Array2[t]
is that in
the second case, all columns have an equal amount of rows, meaning
it can be efficiently projected onto a single one-dimensional
array.
Array index out of bounds exceptions will be caught at run-time.
Arrays implement the standard iterator interface -- thus, it is
possible to iterate over arrays in e.g. a for
loop.
Strings and String Representations
Example:
"I am a string."
Strings are immutable, have reference semantics but not identity.
Thus, it is not possible to compare two strings for identity. For
this, atoms should be used (Atoms have not yet been implemented). C-family escape characters are
supported, e.g. \n
, \t
etc.
The Encore string type is String
. The operations on strings are
described in the String
class in the standard bundle. Because of
the immutability of strings, all operations on strings return new
string objects. Encore does not offer any binary operators on string,
i.e., "4" + "2"
is not well-typed. Instead, write "4".concatenate("2").
Encore provides global print()
and println()
functions that
work similar to C-style printf()
meaning it has a variable
arity. The first argument is a format string, using the special
symbol {}
for the places where the string representation of a
value should appear. Example:
print("{} + {} = {}\n", x, y, string_from_int(x + y))
When x
and y
are integers valued 5 and 7 respectively, this
will print
5 + 7 = 12
Note that the call to string_from_int()
is not necessary.
Converting values into their corresponding string representations
is automatic for the following types:
- primitives
- tuples (when all its element types have a string representation)
- maybe types (when its element type has a string representation)
- futures (when its element type has a string representation)
All other objects must implement the Repr
trait, which requires a
user-defined str_repr()
method that computes and returns the
string representation.
Note that using print()
is only advisable for debugging and toy
programs. Instead, the libraries for asynchronous I/O should be
used.
Ranges
Example:
new Range(1, 10) -- the range [1..10]
new Range(1, 10).by(2) -- the range [1,3,5,7,9]
new Range(1, 10).before() -- the range [1..10)
new Range(1, 10).after() -- the range (1..10]
new Range(1, 10).after().before() -- the range (1..10)
new Range(1, 10).to_a() -- the array [1,2,3,4,5,6,7,8,9,10]
A Range
object is an efficient representation of an ordered
collection of integers. Ranges implement a standard iterator. Their
key use case is to be iterated over.
Ranges can be used to create arrays as demonstrated above.
Note: Ranges have not yet been implemented.
Object Creation
Example:
-- create a new instance of class person and invoke its constructor
-- with two arguments, name and age
new Person(name, age)
-- create a new instance of class Symbol, which does not have a constructor
new Symbol()
Object creation takes the shape of a class name called as a function, similar to Python. This calls the constructor (if any) for the class synchronously for passive objects, and asynchronously for actors.
In the latter case, it is not possible to observe that a constructor has finished by waiting on its return value. But observe that the contructor message is guaranteed to be the first message received by an actor.
Embedding C Code
Examples:
EMBED(int)
int64_t value = 4711;
value;
END
fun perror(s:String) : unit
let cstring = s.data in
EMBED(unit)
fprintf(stderr, "Error: %s\n", cstring);
END
end
end
class String
data : EMBED char * END
end
Encore supports embedding of arbitrary C expressions. For now, Encore compiles to C code, and names of fields and local variables are available in each name space, using a name mangling scheme to protect clashes between generated names in the C code and names in Encore programs.
For plenty of examples on how to use EMBED
, see String.enc
in
modules/standard
.
Return and Forward
Examples:
return 17 -- returns 17
return -- returns ()
forward(this.foo()) -- fulfils the current future with this.foo()
The return
keyword ends the current method or function and
returns a value. If no value is specified, unit is returned.
The keyword is optional in the sense that the last expression to be evaluated by a method or function is its return value, even without an explicit return.
The forward
keyword also ends the current method or function. If
forward
is used in a place where there is no current future, it
works just like return
which blocks until there is a value to
return. Otherwise, a forwarded expression will be used to fulfil
the current future. Consider the following scenario:
active class Link
value:int
next:Maybe[Link]
def sum(aggregate:int) : int
match this.next with
Just(next) => forward(next.sum(aggregate + this.value))
_ => forward(aggregate + this.value)
end
end
end
first ! sum(0)
This is defining a recursive method for calculating the sum of integers in a linked list. The forward keyword lets us chain the future through the entire recursive call to be fulfilled by the last link in the list.
If sum()
is called completely synchronously, i.e., there is no
future, forward
simply works as a return
.
Unit
The type unit
is the unit type — the equivalent of void
in
the C family of languages. A key difference is that unit is
actually a value — which is important in the case of futures. The
following method, when called asynchronously as part of an actor,
returns a Future[unit]
which the caller can query for its
fulfilment. This allows the caller to check whether the print has
completed or not.
The literal for unit is ()
.
def print(s:String) : unit
...
end
Shadowing
Fields are accessed through the this.
prefix — similar for
method calls. This avoids name clashes with local variables and
global functions. Parameters are implicitly val
meaning they
cannot be reassigned. Explicitly declaring a parameter var
is
permitted.
Encore does not allow shadowing of local names, so the following is not allowed:
for i <- Range(1, 10) do
for i <- Range(0, i) do -- the first i will cause an error
print(i)
end
end
Note: the above is currently permitted by the compiler (Issue #713)
However, the following is allowed:
local class Cell[t]
var value : t
def set(value : t) : unit
this.value = value
end
end
Because of the val
-ness of parameters, the following will be a
compile-time error:
local class Cell
var value : int
def set(value : int) : unit
value = 42
end
end
Local variables and local functions are allowed to shadow global
names. For example, if importing a function foo() : bool
into the
top-level, the following code snippets are still correct, and shadow
the top-level foo
with foo
's of their own:
fun bar() : int
foo(42)
where
fun foo(x : int) : int
x
end
end
fun baz() : int
val foo = fun (x : int) => x
foo(42)
end