Elan Language Reference
Instruction Set
This section defines the set of instructions that make up the Elan language and form the building blocks for any program.
There are three categories of instruction, which are distinguished where the instructions of that category are located within a program:
- Global instructions (also referred to simply as ‘globals’) are located directly within a code file.
They are never indented from the left-hand edge, nor may they be located within other instructions.
Three of the globals – main, function, and procedure – are described as ‘methods’ and these defined one or more statements within them.
Four of the globals – record, class, abstract class, and interface – define data structures and these always contain members.
The two remaining globals – constant, and enum – do not contain any further instructions.
- Member instructions (also referred to simply as ‘members’) are located directly within the ‘data structure’ globals: record, class, abstract class, interface.
All members, with the exception of property, define one or more statements within them.
- Statement instructions (also referred to simply as ‘statements’) are located within ‘methods’. Some of these statements may contain other statements.
All instructions are added into a program using the new code ‘selectors’.
A comment is not an instruction: it is ignored by the compiler and does not change how the program works.
Rather, a comment contains information about the program, intended to be read by a person seeking to understand or modify the code.
Every comment starts with the symbol # (known as ‘hash’) followed by some text or a blank line.
Comments may be inserted at the same level as a global,
member, or statement instruction, by entering # from the new code selector.
Every Elan program has a single comment at the top of the file, which is generated by the system and cannot be edited or deleted by the user.
This comment is known as the ‘header’ and gives the version of Elan being run together with other optional information that depend upon your user profile.
Global Instructions
When you navigate to a new code that is at global level (not indented from the left-hand edge of the code pane)
you will be shown the set of globals that you may insert there, namely all or only some of:
main
procedure
function
test
constant
enum
record
class
abstract
interface
#
where abstract is, in this context, short for abstract class.
new code
The specific globals offered will depend upon your user profile.
Main method
A file must have a main method if it is intended to be run as a program. You may, however, develop and test code that does not have a main method, either as a coding exercise or for subsequent use within another program.
The main method defines the start point when a program is run.
The main method does not have to be at the top of the file, but this is a good convention to follow.
The main method may delegate work to one or more procedure or function.
There may not be more than one main method in a file – and the global selector (above) will not show the main option when one already exists in the file.
Example:
+main
variable liname set to [3, 6, 1, 0, 99, 4, 67]expression
call inPlaceRippleSortprocedureName(liarguments)
print liexpression
end main
Procedure
A procedure is a named piece of behaviour that can define parameters which are given inputs via arguments in a call statement.
Unlike a function, a procedure does not return a value.
Also unlike a function, a procedure can have ‘side effects’: indeed it should have side effects, otherwise there would be no point in calling it!
For this reason the statements within a procedure can:
- Include print statements (or methods).
- include input methods or other ‘system’ methods (such as a random number generation).
- call other procedures (or itself if ‘recursion’ is required).
- Re-assign a parameter, provided that parameter definition is preceded by the keyword out, as in this example:
+procedure inPlaceRippleSortname>(out arr as List<of Int>parameter definitions)
variable changesname set to trueexpression
variable lastCompname set to arr.length() - 2expression
+repeat
set changesvariableName to falseexpression
+for ivariableName from 0expression to lastCompexpression step 1expression
+if arr[i] > arr[i + 1]condition then
let tempname be arr[i]expression
call arr.putAtprocedureName(i, arr[i + 1]arguments)
call arr.putAtprocedureName(i + 1, temparguments)
set changesvariableName to trueexpression
end if
end for
set lastCompvariableName to lastComp - 1expression
end repeat when not changescondition
end procedure
Procedures are executed by means of a call statement, for example:
+main
variable liname set to [3, 6, 1, 0, 99, 4, 67]expression
call inPlaceRippleSortprocedureName(liarguments)
print liexpression
end main
Function
A function is a named piece of behaviour that can define parameters which are given inputs via arguments when reference to the function occurs in an expression.
Unlike a procedure, a function returns a value. Also unlike a procedure, a function can have no ‘side effects’ and may not depend on any System methods.
Example of a function:
+function scorename(g as Gameparameter definitions) returns IntType
return g.body.length() - 2expression
end function
Parameters
Parameters for both procedures and functions are defined in exactly the same way: each parameter definition takes the form:
<name> as <Type>
for example:
age as Intparameter definitions
Recursion
Procedures and functions may be called or referenced recursively. For example, a simple factorial calculation:
+function factorialname(n as Intparameter definitions) returns IntType
return (if n > 1 then n*factorial(n - 1)
else 1)expression
end function
Test
A test is a set of assertions, at the global level, about the output of functions. Example of a test, from the binary search demo program:
+test
let li1name be ["lemon", "lime", "orange"]expression
assert binarySearch(li1, "lemon")computed value is trueexpected value pass
assert binarySearch(li1, "lime")computed value is trueexpected value pass
assert binarySearch(li1, "orange")computed value is trueexpected value pass
assert binarySearch(li1, "pear")computed value is falseexpected value pass
let li2name be ["lemon", "orange"]expression
assert binarySearch(li2, "lemon")computed value is trueexpected value pass
assert binarySearch(li2, "orange")computed value is trueexpected value pass
assert binarySearch(li2, "pear")computed value is falseexpected value pass
let li3name be ["lemon"]expression
assert binarySearch(li3, "lemon")computed value is trueexpected value pass
assert binarySearch(li3, "lime")computed value is falseexpected value pass
let li4name be empty List<of String>expression
assert binarySearch(li4, "pear")computed value is falseexpected value pass
end test
Notes:
- Elan tests are designed to test functions only. It is not possible to call a procedure or main routine within a test. Nor is it possible to use any System method (the same rule as for a function).
- A test may optionally be given a name or description in free-form text, just like a comment, which plays no role in the execution of the test. You might give the test the same name as a function that it is testing, or you might describe a particular scenario that is being tested.
- test methods may be written anywhere in the code, at the global level.
- A test method may contain any number of assert statements. When tests are run, the test runner (part of the Elan IDE), will attempt to run all assert statements and show each one's pass or fail outcome alongside. However, if the test hits a runtime error (as distinct from an assert failure) then execution of the test will stop and remaining asserts will be shown as ‘not run’.
- In addition to assert statements, a test may contain any other statements that may be added into a function (except return).
- All asserts should be at the top level within the test frame; none may be put into a loop structure.
Testing Float values
When testing Float values it is recommend that you always use the round function to round the computed result to a fixed number of decimal places. This avoids rounding errors and is easier to read:
+test
assert sqrt(2).round(3)computed value is 1.414expected value pass
end test
Testing for runtime errors
If the expression you are testing causes a runtime error then the error will be displayed in the red fail message:
+test
let aname be [5, 1, 7]expression
assert a[0]computed value is 5expected value pass
assert a[2]computed value is 7expected value pass
assert a[3]computed value is 0expected value actual: Out of range index: 3 size: 3
end test
If this occurs, mark the tests that you added since the last successful test run with ignore (see below), and then remove the ignores one by one until the cause is identified and can be fixed.
Marking a test with ‘ignore’
It is possible to mark a test with the ignore keyword, by selecting the test frame and then hitting Ctrl-i, as in this example:
+ignore test
let g1name be newGame(new Random())expression
let g2name be newApple(g1)expression
let g3name be clockTick(g2, "s")expression
assert g3.headcomputed value is newSquare(22, 16)expected value not run
end test
When a test is marked with ignore, that test will not be executed when the tests are run, and its result will be shown as ‘not run’. The overall test status will also show in the ‘warning’ status (amber colour), even if all the tests that did run passed. This is to discourage you from leaving a test marked ignore for long.
The principal reason for marking a test ignore is when either the test code, or code in any function being called, does not terminate. This typically means that there is a loop (or a recursive call) with no exit condition, or where the exit condition is never met.
If you do create such code without realising it, then when the tests are executed the test runner will ‘time out’ after a few seconds (most tests will pass in milliseconds), and an error message will be shown in the System info pane. The test that caused the timeout will automatically then be marked ignore. Your priority should then be to identify the cause of the timeout and attempt to fix it before then restoring the test by selecting its frame and hitting Ctrl-i {which is a toggle for setting and unsetting an ignore status).
Constant
A constant defines a named value that cannot change and is always defined at global level (directly within a file) and is global in scope.
- A constant may not be defined within any method. (However, see).
- The name of a constant follows the rules for an Identifier.
- The value to which a constant is set must be a Literal value of one of the following Types: Int, Float, Boolean, String, Dictionary or DictionaryImmutable.
Examples:
+constant phiname set to 1.618literal value or data structure
+constant maxHitsname set to 10literal value or data structure
+constant warningMsgname set to "Limit reached"literal value or data structure
+constant fruitname set to {"apple", "orange", "banana"}literal value or data structure
+constant blackname set to 0x000000literal value or data structure
+constant redname set to 0xff0000literal value or data structure
+constant coloursname set to {Suit.spades:black, Suit.hearts:red, Suit.diamonds:red, Suit.clubs:black}literal value or data structure
+constant scrabbleValuesname set to {"A":1, "B":3, "C":3, "D":2, "E":1, "F":4, "G":2, "H":4, "I":1, "J":8, "K":5, "L":1, "M":3, "N":1, "O":1, "P":3, "Q":10, "R":1, "S":1, "T":1, "U":1, "V":4, "W":4, "X":8, "Y":4, "Z":10}literal value or data structure
In the colours example above, Suit is an Enum.
Enum
An enum – short for ‘enumeration’ – is the simplest form of ‘user-defined Type’.
It specifies a set of values, each of which is defined as a name, such that a named value of Type enum necessarily always holds one of those values.
enums are read-only: once they have been defined it is not possible to add, remove, or update their values.
Examples:
+enum SuitName spades, hearts, diamonds, clubsvalues
+enum ActionName stand, drawvalues
+enum OutcomeName undecided, win, lose, draw, winDoublevalues
+enum StatusName pending, playing, standing, blackjack, bustvalues
Type name
The name given to an enum (see below), which must begin with an upper case letter, is used as the Type name when passing a value to or from a procedure or function.
Using an enum
The value is specified by the Type name for the specified enum, followed by a dot and the value name, for example:
variable xname set to Status.pendingexpression
Record
A record is a user-defined data structure that is given a Type name (which must begin with an upper case letter).
The record defines one or more properties, each of which has a name (starting with a lower case letter) and a Type.
The Type of a property may be any simple value Type, or a ListImmutable, or another Type of record ( or even the same Type of record).
Note that the Type record has some similarity to a class in that:
- Both are user-defined data structures
- Both are given a ‘Type name’
- Both may define one or more properties, each with a name and Type
- Both may define encapsulated methods
However a record differs from a class in that:
- A record is immutable (like a ListImmutable or a String). You can create a copy with specified differences but you cannot modify a property on a given instance.
- A record instance may be created or copied using a with clause, whereas with may not be used on a class instance.
- A record does not define a constructor
- A record may define only function methods, since procedure methods would imply the ability to mutate the record.
Examples:
+record SquareName
property xname as IntType
property yname as IntType
end record
+record GameName
property headname as SquareType
property bodyname as ListImmutable<of Square>Type
property priorTailname as SquareType
property applename as SquareType
property isOnname as BooleanType
property rndname as RandomType
property graphicsname as BlockGraphicsType
property keyname as StringType
end record
Having defined a record Type, such as Game above, you can create as many instances as you wish using the following syntax to specify the values:
let g1name be new Game() with
head set to newSquare(22, 15),
key set to "d",
isOn set to trueexpression
Note that you are not required to provide a value for each property because, where a property is not specified in the ‘with clause’ (as above), that property will be given the empty (default) value of the correct Type.
You can then read the values from the properties using ‘dot syntax’ for example:
print sq.sizeexpression
record Types are immutable: the properties on an instance may not be changed, directly.
However, you can easily create another instance that is a copy of the original,
with all the same property values except for any specific changes made in another with clause.
The newly-minted copy (with changes) must be assigned to a new named value. For example:
let sq1name be new Square() with
x set to 3.5,
y set to 4.0,
size set to 1.0expression
let sq2name be copy sq1 with
size set to 2.0,
colour set to redexpression
Or even to the same name if that name is a variable:
variable aname set to new Square() with
x set to 3.5,
y set to 4.0expression
set avariableName to copy a with
x set to 3.7expression
This last example shows how you enter the comma-separated with clauses. The earlier examples show how the Editor displays a set of with clauses.
If you want to use one or more existing property values in order to determine a new value, the property names must be prefixed with the name of the instance being copied, for example:
let sq2name be copy sq1 with
size set to sq1.size + 3expression
Record deconstruction
A record may be ‘deconstructed’, meaning that its properties are read into separate variables using the same syntax as for deconstructing a Tuple. For example, assuming that Square is a record defined as in the example above, then this code:
let x, y, size, colourname be mySquareexpression
will read the properties into the four names defined.
When deconstructing, the names of the values must match the names of the properties of the record. However, the ordering of the names does not have to match the order in which the properties are defined in the record.
Class
See also: Inheritance
A class is a user-defined Type offering far richer capability than an enum.
Note that a record is in some ways similar to a class but simpler: it defines properties, but has no constructor and no methods. See Working with records.
Definition
Here is an example of class definition, taken from the Snake OOP demo program:
+class AppleName inherits ClassName(s)
property locationname as SquareType
+procedure newRandomPositionname(snake as Snakeparameter definitions)
+repeat
let ranXname be randomInt(0, 39)expression
let ranYname be randomInt(0, 29)expression
set property.locationvariableName to new Square(ranX, ranY)expression
end repeat when not snake.bodyCovers(property.location)condition
end procedure
+function updateGraphicsname(gr as BlockGraphicsparameter definitions) returns BlockGraphicsType
return property.location.updateGraphics(gr, red)expression
end function
end class
A class must have a name that, like any other Type, begins with an upper case letter.
A class may define:
- One or more properties – see Property
- function methods – see Function method
- procedure methods – see Procedure method
- a constructor which may be used for setting up the values of properties. The constructor may optionally define parameters to force the calling code to provide initial values. However, it is not necessary to add a constructor if you have no need to initialise properties. Code in the constructor may make use of any functions, and follows the same constraints as a function (i.e. it may not call any procedure, whether defined on the class or outside).
Using a class
A class is instantiated using the keyword new followed by the class name and brackets, which should enclose the comma-separated arguments required to match the parameters (if any) defined on the constructor for that class. For example (also from the Snake OOP demo):
+constructor(parameter definitionsparameter definitions)
let tailname be new Square(20, 15)expression
set property.currentDirvariableName to Direction.rightexpression
set property.bodyvariableName to [tail]expression
set property.headvariableName to tail.getAdjacentSquare(property.currentDir)expression
set property.priorTailvariableName to tailexpression
end constructor
The created instance may then be used within expressions, like any other variable.
Inheritance
An ordinary class (also known as a ‘concrete class’) may optionally inherit from just one abstract class
but may additionally inherit from any number of interfaces.
The concrete class must define for itself a concrete implementation of every abstract member defined in the abstract class or any interfaces that it inherits from, directly or indirectly.
Notes:
- An abstract class must be declared in the code above any class that inherits from it. This is the only case where the order of definition (of global constructs) matters.
- The abstract class (if any) and the interfaces (if any) that a concrete class inherits from may not contain duplicates of any abstract member. Any duplicated definitions in the hierarchy will result in a compile error. If such duplications arise, you should factor out the common member definitions, and move them up the hierarchy or into new interfaces inherited by the interfaces and/or classes that need them.
- Inheritance hierarchies must form a tree, that is you must avoid creating a ‘circular’ dependency where, for example, Type A inherits from Type B, which inherits from Type C, which inherits from Type A.
- The various ‘super-Types’ (abstract classes and interfaces) that a concrete class inherits from must not define conflicting members, e.g. members with the same name but having different Type signatures.
Abstract Class
See also: Inheritance
An abstract class may not be instantiated (and hence may not define a constructor). It may define concrete members i.e.:
- a property
- a function
- a procedure
As with a concrete class, any of these members may be made private, after the corresponding frame has been added, by selecting that member frame and pressing Ctrl-p.
These concrete members are automatically inherited by any subclass, but they may not be overridden (re-defined) by the subclass. Therefore you should define concrete members only if they are intended to work identically on every subclass.
You may also define abstract methods on an abstract class, i.e. abstract property, abstract function, abstract procedure. Such methods define only the signature of the method, not the implementation (body), therefore they have no end statement. For example:
abstract function calculateDiscount() as Float
If you wish to have several subclasses of an abstract class that share a common implementation for a method, but require that some of the subclasses can define a different implementation, then you should:
- Define the method as abstract on the superclass.
- Define a concrete implementation on the superclass with a similar, but slightly different, name e.g. by adding a prefix such as: default.
- Each subclass must then define its implementation of the abstract method, but the ones needing a common implementation can be just one line, delegating responsibility up to the ‘default’ method on the superclass.
Interface
See also: Inheritance
An interface is similar to an abstract class, with the difference that it may define only abstract members. The advantage of using an interface instead of an abstract class is that a concrete class can inherit from multiple interfaces. See Inheritance.
An interface may inherit only from other interfaces.
Important: An interface must not re-declare abstract interfaces that are defined in any interface it inherits from, directly or indirectly.
Member Instructions
When you navigate to a new code that is at member level
(located directly within a record, class, abstract class, or interface)
you will be shown the set of statements that may be inserted there, for example:
new code
The specific members offered will depend upon the context, and/or upon your user profile.
The full set of entries is shown here, with links to explanations below:
constructor,
property,
procedure,
function,
abstract property,
abstract procedure,
abstract function,
private property,
private procedure,
private function,
#
Constructor
A concrete class may define a single constructor, which may:
- initialise any properties with fixed values
- define one or more parameters, which are then used to initialise properties
If a class does define a constructor, and the constructor defines any parameters, then when the class is instantiated (using new) then values of
the correct types must be provided, for example, if the class Snake defines this constructor:
+constructor(x as Int, y as Intparameter definitions)
set property.xvariableName to xexpression
set property.yvariableName to yexpression
end constructor
then it may be instantiated like this:
let tailname be new Square(20, 15)expression
Property
Examples:
property height as Int
property board as Board
property head as Square
property body as [Square]
- A property is defined on a Class and must specify a name (conforming to the rules for an Identifier and a Type.
- A property may be marked private, in which case it is visible only to code within the class and, if defined on an abstract class, within its subclasses. This is done by selecting the property frame and pressing Ctrl-p. (Pressing these keys again will remove the private modifier).
- If not marked private, a property may be read but not be written to. Properties may only be modified from outside the class by means of a Procedure method.
- A property may be given an initial value in the constructor.
If the property is not initialised within the constructor then it will automatically be given the empty value for that Type. You may test whether a property contains this default value by writing e.g.:
if head is empty Square
- Whenever you wish to access a property from within a method (or from within the constructor) on the same class, then the name of the property must be prefixed with the ‘qualifier’: property. (‘property-dot’). This applies whether you are reading or setting the property. By this means you can have a method parameter with the same name as a property, but they are unambiguous, because the property must be prefixed. A common pattern is to use the same name in a ‘setter’ method, for example:
constructor(board as Board)
set property.board to board
end constructor
procedure setHeight(height as Int)
set property.height to height
end procedure
Procedure Method
A ‘procedure method’ follows the same syntax and rules as a global procedure. The differences are:
- A procedure method, like a function method, is always referenced (used) by code outside the class using ‘dot syntax’ on an instance.
- A procedure method may read, or write to, any property defined on the class.
- A procedure method may be marked private, in which case it is visible only to code within the class and, if defined on an abstract class, within its subclasses. This is done by selecting the property frame and then pressing Ctrl-p. (Pressing these keys again will remove the private modifier).
Function Method
A function method follows the same syntax and rules as a global function>. The differences are:
- A function method is always referenced (used) by code outside the class using ‘dot syntax’ on an instance.
- A function method may directly reference (read only) any property defined on the class as though it were a variable/parameter.
- A function method may be marked private, in which case it is visible only to code within the class and, if defined on an abstract class, within its subclasses. This is done by selecting the property frame and then pressing Ctrl-p. (Pressing these keys again will remove the private modifier).
- asString() method
- asString method. This is just a regular function method with a specific name, no parameters and returning a String. If defined for a class, then if an instance of the class is printed, the asString function method will automatically be used. Typically asString will return a string made up of one or more of the property values, perhaps with additional text, or the results of function calls.
Abstract Property
An abstract property may be defined only on an abstract class. Any concrete sub-class must then implement a concrete (regular) property to match.
Abstract Procedure Method
An abstract procedure method may be defined only on an abstract class. Any concrete sub-class must then implement a concrete (regular) property to match.
Abstract Function Method
An abstract function method may be defined only on an abstract class. Any concrete sub-class must then implement a concrete (regular) property to match.
Statement Instructions
When you navigate to a new code that is at statement level
(located within a global or a member instruction)
you will be shown the set of statements that may be inserted there, for example:
new code
The specific statements offered will depend upon the context, and/or upon your user profile.
The full set of entries is shown here, with links to explanations below:
assert
call
each
else
for
if
let
print
repeat
set
throw
try
variable
while
#
Assert statement
Procedure Call
Each loop
Else clause
For loop
The loop counter variable does not have to have been defined in a variable statement.
The three defining values (from, to, and step) must all be integer, positive or negative.
They may be defined by literal integers, variables of Type Int, or expressions that evaluate to an integer.
However, if you require a negative step then the literal value, variable, or expression must start with a negative sign as this is needed at compile time to determine the nature of the exit condition. So if you have a variable s that holds a negative value to be used as the step, then you will need to write something like the following:
variable s set to -3
for i from 100 to 0 step -(-s)
..
end for
If statement
See also if expression
Example 1:
+if head is applecondition then
call setAppleToRandomPositionprocedureName(apple, bodyarguments) 'setAppleToRandomPosition' is not defined
else if
call body.removeAtprocedureName(0arguments)
end if
Example 2:
if item is value then
set result to true
else if item.isBefore(value) then
set result to binarySearch(list[..mid], item)
else
set result to binarySearch(list[mid + 1..], item)
end if
Notes:
- The else clause is optional
- You can add as many else if clauses as you wish, but only one unconditional else (which, if present, must be the last clause).
Let statement
Print statement
The simplest way to print is with the print statement. For example:
+main
print "Hello"expression
let aname be 3expression
let bname be 4expression
print a*bexpression
print "{a} times {b} equals {a*b}"expression
end main
Note:
- The last line in the example above uses an interpolated string. Arguments placed within curly braces are evaluated before printing, and these may be separated by literal text and punctuation as needed. This is one recommended way to print more than one value on a line. The other way is to use print procedures.
Repeat loop
Set statement
Throw statement
You can deliberately generate, or ‘throw’, an exception when a specific circumstance is identified with a throw statement, for example:
throw exception "something has happened"
Try statement
Where another piece of code might throw an exception, for example when calling a System method that is dependent upon external conditions, it may be executed within a try statement, for example:
+try
call fooprocedureName()
print "not caught"expression
+catch exception in evariableName
print eexpression
end try
The variable holding the exception (by default named e, but this may be changed by you) is of Type String. You may compare the exception message to one or more expected messages and, if the message does not match an expected exception, you may choose to throw the exception ‘up’, as in this example:
+try
call fooprocedureName()
print "not caught"expression
+catch exception in evariableName
+if e isnt "an expected message"condition then
throw exception emessage
end if
end try
Variable statement
While loop
Expressions
One of the most important constructs in programming is the ‘expression’. An expression is evaluated to return a value. An expression is made up of the following possible elements:
- Literal value
- Named value
- Operator (including brackets)
- Function call
which this chapter describes.
Literal value
A literal value is where a value is written ‘literally’ in the code, such as 3.142 – in contrast to a value that is referred to by a name.
The following data Types may be written as literal values (follow the links to view the form of each literal value):
Int, Float, Boolean, String, List, ListImmutable, Dictionary, DictionaryImmutable, Tuple
Named value
A named value is a value that is associated with a name rather than being defined literally in code. There are various kinds of named value:
Constant, let statement, variable statement, Parameter passing, enum statement
Identifier
For all kinds of named values, the name must follow the rules for an ‘identifier’. It must start with a lower case letter, followed by any combination of lower case and upper case letters, numeric digits, and the _ (underscore) symbol. It may not contain spaces or other symbols. Once a named value has been defined, it can be referred to by the name.
Scoping and name qualification
With the exception of a constant (below), which is global in scope, named values are always ‘local’: their scope is confined to the method in which they are defined.
Elan allows local named values to be defined with the same name as a constant, function, or procedure defined at global level or defined in the standard library. In such cases, when the name is used within the same method, then it will refer to the local definition. If you have done this, but then need to access the constant, function, or procedure with the same name, then you can simply prefix the use of the name with a ‘qualifier’ of either global. or library. as appropriate.
Indexed Value
If a variable is of an indexable Type, then an index or index range may be applied to the variable within an expression. For example:
variable a set to "Hello World!"
print a[4] → o
print a[4..] → o World!
print a[..7] → Hello W (since the upper bound of a range is exclusive)
print a[0..4] → Hell (for the same reason)
See also: Using an List, Using a Dictionary
Important: unlike in many languages, indexes in Elan (whether, single, multiple, or a range) are only ever used for reading values.
Writing a value to a specific index location is done through a method such as in these examples:
putAt on a List
withPut on a ListImmutable
putAtKey on a Dictionary
withPutKey on a DictionaryImmutable
Operators
Arithmetic operators
Arithmetic operators can be applied to Float or Int arguments, but the result is always a Float:
2^3 → 8
2/3 → 0.666...
2*3 → 6
2 + 3 → 5
2 - 3 → -1
11 mod 3 → 2 (integer remainder)
11 div 3 → 3 (integer division)
Arithmetic operators follow the conventional rules for precedence i.e. ‘BIDMAS’ (or ‘BODMAS’).
When combining div or mod with any other operators within an expression, insert brackets to avoid ambiguity e.g.:
(5 + 6) mod 3
The minus sign may also be used as a unary operator, and this takes precedence over binary operators so:
2*-3 → -6
Note that the Elan editor automatically puts spaces around the + and – binary operators, but not around ^, / or *. This is just to visually reinforce the precedence.
Logical operators
Logical operators are applied to Boolean arguments and return a Boolean result.
and and or are binary operators
not is a unary operator.
The operator precedence is not → and → or, so this
example, which implements an ‘exclusive or’, need not use brackets and can rely on the operator precedence:
+function xorname(a as Boolean, b as Booleanparameter definitions) returns BooleanType
return a and not b or b and not aexpression
end function
Equality testing
Equality testing uses the is and isnt keywords with two arguments. The arguments may be of any Type.
- a is b returns true, if a and b are both of the same Type and their values are equal. The only exception is that if one argument is of Type Float and the other is of Type Int, then is will return true if their values are the same, i.e. are the same whole number.
- isnt returns the opposite of is
Note that in Elan equality testing is always ‘equality by value’; there is no such thing as ‘equality by reference’.
Note
- Where a binary operator is expected, as soon as you type is the editor will automatically insert a space after it. To enter isnt you need to delete the space (using the Backspace key) and then type nt.
Numeric comparison
The numeric comparison operators are:
> for greater than
< for less than
>= for greater than or equal to
<= for less than or equal to
Each is applied to two arguments of Type Float, but any variable or expression that evaluates to an Int may always be used where a Float is expected.
Notes:
- These operators cannot be applied to strings. Use the dot methods isBefore and isAfter to compare strings alphabetically. See Dot methods on a String.
- Where a binary operator is expected, as soon as you type < or > the editor will automatically insert a space after it. To enter <= or >= you need to delete the space (using the Backspace key) and then type =.
Combining operators
You can combine operators of different kinds, e.g. combining numeric comparison with logical operators in a single expression. However the rules of precedence between operators of different kinds are complex. It is strongly recommend that you always use brackets to disambiguate such expressions, for example:
(a > b) and (b < c)expression
(a + b) > (c - d)expression
Function call
An expression may simply be a function call, or it may include one or more function calls within it. Examples:
print sinDeg(30)expression
variable xname set to sinDeg(30)^2 + cosDeg(30)^2expression
variable namename set to inputString("Your name")expression
print name.upperCase()expression
Notes:
- The third example above is not strictly a function call, but is a ‘system method’ call.
System methods may be used only within the main routine or a procedure, because they have external dependencies or side effects.
- In the fourth example, upperCase is a ‘dot method’ that may be applied to any instance (variable or literal) of Type String.
See Dot methods on a String.
Lambda
A lambda is lightweight means to define a function ‘in line’. You typically define a lambda:
Example:
+function liveNeighboursname(cells as List<of Boolean>, c as Intparameter definitions) returns IntType
let neighboursname be neighbourCells(c)expression
let livename be neighbours.filter(lambda i as Int => cells[i])expression
return live.length()expression
end function
Notes:
- Although a lambda is commonly defined ‘inline’ (as shown above) it is possible to assign a lambda to a variable and hence to re-use it within the scope of that variable.
- Ana although a lambda will usually define at least one parameter, it is possible to define a lambda with no parameter – just returning an expression – in which case it acts just like a locally defined variable, but with the advantage (useful in rare circumstances) that the expression is evaluated ‘lazily’ i.e. only when the lambda is used.
The following example uses both these techniques within a function:
+function safeSquareRootname(x as Floatparameter definitions) returns FloatType
let rootname be lambda => sqrt(x)expressionname as Type, ...
return if x < 0 then 0
else root()expression
end function
If expression
The ‘if expression’ is in certain respects similar to an If statement, but with the following differences:
- It is written entirely within a single expression. This is possible because the if expression always returns a value.
- There is always a single then and a single else clause, and each clause contains just a single expression. The if expression returns the result of evaluating one of these two expressions, according to whether the condition evaluates to true or false.
Here are three examples:
return if c < 1160 then c + 40
else c - 1160expression
return if isGreen(attempt, target, n) then setChar(attempt, n, "*")
else attemptexpression
return if attempt[n] is "*" then attempt
else if isYellow(attempt, target, n) then setChar(attempt, n, "+")
else setChar(attempt, n, "_")expression
Elan Language Reference go to the top