Elan Language Reference

Instruction Set

This section defines the set of instructions that make up the Elan language, and which form the building blocks for any program.

There are three categories of instruction, which are distinguished where the instructions of that categeory 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'.

    Comments

    A comment is not an instruction: it will be 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 program. Every comment starts with the symbol # (known as 'hash') followed by some text or a blank line. Comments can be inserted by a programmer 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 may not 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 depending 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 may be inserted there, for example:

    new code main procedure function test constant enum record class abstract interface #

    The specific globals offered will depend upon your user profile. The full set of globals is shown here with links to explanations below:

    main, procedure, function, test, constant, enum, record, class, abstract (in this context, short for abstract class), interface #

    Main method

    A file must have a main method if it is intended to be run as 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.

    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 1 variable liname set to [3, 6, 1, 0, 99, 4, 67]expression2 call inPlaceRippleSortprocedureName(liarguments)3 print liexpression4 end main

    The main method may delegate work to one or more procedure or function.

    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 Example:
  • +procedure inPlaceRippleSortname>(out arr as Array<of Int>parameter definitions)5 variable changesname set to trueexpression6 variable lastCompname set to arr.length() - 2expression7 +repeat8 set changesvariableName to falseexpression9 +for ivariableName from 0expression to lastCompexpression step 1expression10 +if arr[i] > arr[i + 1]condition then11 let tempname be arr[i]expression12 call arr.putAtprocedureName(i, arr[i + 1]arguments)13 call arr.putAtprocedureName(i + 1, temparguments)14 set changesvariableName to trueexpression15 end if end for set lastCompvariableName to lastComp - 1expression16 end repeat when not changescondition end procedure

    Procedures are executed within a call statement, for example:

    +main1 variable liname set to [3, 6, 1, 0, 99, 4, 67]expression2 call inPlaceRippleSortprocedureName(liarguments)3 print liexpression4 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?30 return g.body.length() - 2expression?31 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 Int

    Recursion

    Procedures and functions may be called or referenced recursively. For example, a simple factorial calculation: +function factorialname(n as Intparameter definitions) returns IntType3 return (if n > 1 then n*factorial(n - 1)
    else
    1)
    expression
    4
    end function

    Test

    Example of a Test:

    +test optional description26 let li1name be ["lemon", "lime", "orange"]expression27 assert binarySearch(li1, "lemon")computed value is trueexpected value pass28 assert binarySearch(li1, "lime")computed value is trueexpected value pass29 assert binarySearch(li1, "orange")computed value is trueexpected value pass30 assert binarySearch(li1, "pear")computed value is falseexpected value pass31 let li2name be ["lemon", "orange"]expression32 assert binarySearch(li2, "lemon")computed value is trueexpected value pass33 assert binarySearch(li2, "orange")computed value is trueexpected value pass34 assert binarySearch(li2, "pear")computed value is falseexpected value pass35 let li3name be ["lemon"]expression36 assert binarySearch(li3, "lemon")computed value is trueexpected value pass37 assert binarySearch(li3, "lime")computed value is falseexpected value pass38 let li4name be empty Array<of String>expression39 assert binarySearch(li4, "pear")computed value is falseexpected value pass40 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 within a function).
  • A test may be given a name and/or description, which is free-form text, just like a comment. This name/description is optional and 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 (file) level.
  • A test method may contain multiple assert statements. When tests are run, the test runner (part of the Elan IDE) will attempt to run all assert statements and show the pass/fail outcome alongside each one. 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 should 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 round()optional description1 assert sqrt(2).round(3)computed value is 1.414expected value pass2 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 optional description1 let aname be [5, 1, 7]expression2 assert a[0]computed value is 5expected value pass3 assert a[2]computed value is 7expected value pass4 assert a[3]computed value is 0expected value actual: Out of range index: 3 size: 35 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 for example:

    +ignore test clockTickoptional description1 let g1name be newGame(new Random())expression2 let g2name be newApple(g1)expression3 let g3name be clockTick(g2, "s")expression4 assert g3.head()computed value is newSquare(22, 16)expected value5 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, would 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 on the Console. The test that caused the time-out will automatically then be marked ignore. Your priority should then be to identify the cause of the time-out and attempt to fix it before then restoring the test by selecting the test 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.

    A constant 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 [missing hyperlink]).

    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 structure1 +constant maxHitsname set to 10literal value or data structure2 +constant warningMsgname set to "Limit reached"literal value or data structure3 +constant fruitname set to {"apple", "orange", "banana"}literal value or data structure4 +constant blackname set to 0x000000literal value or data structure5 +constant redname set to 0xff0000literal value or data structure6 +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 structure7 +constant coloursname set to {Suit.clubs:black, Suit.diamonds:red, Suit.hearts:red, Suit.spades:black}literal value or data structure 8

    (In the last 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 that enum Type must always hold one of those values.

    Examples:

    +enum ActionName stand, drawvalues245 +enum OutcomeName undecided, lose, draw, win, winDoublevalues246 +enum StatusName pending, playing, standing, blackjack, bustvalues247

    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 x set to Status.ready

    Notes

  • enums are read-only: once they have been defined it is not possible to add, remove, or update their values.
  • 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 (as in the example above), or a data structure such , another Type of record (or even the same Type of record).

    Note that a record Type has some similarity to a class:

  • Both are user-define 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 is different from a class in that:

  • A record is immutable (like a List 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 - because procedure methods would imply the ability to mutate the record.
  • Examples:

    +record SquareName5 property xname as FloatType6 property yname as FloatType7 +function asStringname(parameter definitions) returns StringType8 return "Sq {property.x}, {property.y}"expression9 end function end record +record GameName 1 property headname as SquareType2 property bodyname as List<of Sqaure>Type3 property priorTailname as SquareType4 property applename as SquareType 5 property applename as SquareType 6 property isOnname as BooleanType7 property rndname as RandomType8 property graphicsname as BlockGraphicsType9 property keyname as StringType10 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 true
    expression
    52

    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 2

    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.0
    expression
    2
    let sq2name be copy sq1 with
    size set to 2.0,
    colour set to red
    expression
    4
    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.0
    expression
    2
    set avariableName to copy a with
    x set to 3.7
    expression
    3

    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 name(s) must be prefixed with the name of the instance being copied, for example:

    let sq2name be copy sq1 with
    size set to sq1.size + 3
    expression
    4

    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 2

    will read the properties into the four names defined.

    Note

  • 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.

    (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 Apple
    constructor(board as Board)
    set property.board to board
    end constructor
    property board as Board
    property location as Square
    procedure newRandomPosition(snake as Snake)
    repeat
    variable ranX set to randomInt(0, board.width - 1)
    variable ranY set to randomInt(0, board.height - 1)
    set location to new Square(ranX, ranY)
    end repeat when not snake.bodyCovers(location)
    end procedure
    function updateGraphics(gr as BlockGraphics) returns BlockGraphics
    return gr.withBlock(location.x, location.y, red)
    end function
    end class

    Notes

    A class must have:

  • A name that, like any other Type, must begin 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. 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):

    variable board set to new Board(40, 30)
    variable currentDirection set to Direction.up
    variable snake set to new Snake(board, currentDirection)
    variable apple set to new Apple(board)

    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 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.
  • Also, 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 class and/or interfaces) that a concrete class inherits from may 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 classes 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. having 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 constructor function procedure property private... #

    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)67 set property.xvariableName to xexpression68 set property.yvariableName to yexpression69 end constructor

    then it may be instantiated like this:

    let tailname be new Square(20, 15)expression15

    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 call each for if let print repeat set throw try variable while #

    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 bave 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

    Example1:

    +if head is applecondition then2 call setAppleToRandomPositionprocedureName(apple, bodyarguments) 'setAppleToRandomPosition' is not defined3 else if4 call body.removeAtprocedureName(0arguments) 5 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

    +main1 print "Hello"expression?2 let aname? be 3expression?3 let bname? be 4expression?4 print a*bexpression?5 print "{a} times {b} equals {a*b}"expression?6 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 foo()
    print "not caught"
    catch exception in e
    print e
    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 foo()
    print "not caught"
    catch exception in e
    if e isnt "An expected message" then
    throw exception e
    end if
    end try

    Variable statement

    While loop

    Types defined as part of the language

    Int

    An integer or whole number, i.e. one that has no fractional component.

    Type name

    Int

    Defining a literal integer

    variable meaningOfLife set to 42

    Default value

    0

    Constraints

  • Maximum value: 253 – 1 which is just over 9 × 1015
  • Minimum value: – (253 – 1)
  • If either limit is exceeded the number will automatically be represented as a Float, with possible loss of precision.

    Notes

  • A value assigned to an Int may be expressed in decimal or, if preceded by 0x, in hexadecimal. Hexadecimal is useful for defining colours and Unicode codepoint values.
  • An Int may always be passed as an argument into a method that specifies a Float.
  • Float

    A ‘floating-point number’, i.e. a number that may have both integer and fractional parts.

    Type name

    Float

    Defining literal floating-point value

    variable a set to 1.618

    Constraints

    Since Elan compiles to JavaScript, the constraints on floating point numbers are those of JavaScript:

  • Maximum value: just over 1 × 10308
  • Minimum value: approximately 5 × 10-324
  • For greater detail, refer to the official JavaScript documentation

    Notes

    A variable that has been defined as being of Type Float may not be passed as an argument into a method that requires an Int, nor as an index into an Array, even if the variable contains no fractional part. However, it may be converted into an Int before passing, using the functions floor() or ceiling():
    floor() returns the integer value left by removing any fractional part, and
    ceiling() returns the lowest integer greater than the Float value if does have a fractional part.

    If you wish to define a variable to be of Type Float but initialise it with an integer value then add .0 on the end of the whole number, for example:
    variable a set to 3.0.

    Boolean

    A Boolean value is either true or false.

    Type name

    Boolean

    Defining a literal Boolean

    variable a set to true

    true and false must be written lower-case

    Default value

    false

    String

    A String represents ‘text’ i.e. a sequence of zero or more characters.

    Type name

    String

    Defining a literal string value

    variable a set to "Hello"

    Strings are always delineated by double-quotes.

    Default value

    "", known as ‘the empty string’.

    Notes

  • As in most programming languages, strings are immutable. When you apply any operation or function with the intent of modifying an existing string, the existing string is never modified. Instead, the operation or function will return a new string that is based on the original, but with the specified differences.
  • Strings may be appended to using the plus operator, for example
    print "Hello" + " " + "World"

    A newline may be inserted within a string as \n, for example:
    print "Hello\nWorld"

  • You may insert single-quotes ' within a string.
  • Interpolated string

    Elan strings are automatically interpolated. This means that you may insert the values of variables or simple expressions within a string by enclosing them in curly braces. For example (assuming that the variables a and b are already defined as integers) :
    print "{a} times {b} equals {a*b}"

    You cannot include the characters ", {, or } directly within a literal string because of their special meanings. Instead, you use the constants quotes, leftBrace and rightBrace respectively:
    print "This is double quote mark: " + quotes
    Alternatively, you can insert their Unicode codepoints by means of the unicode() standalone function:
    print "This is a double quote mark: " + unicode(34)

    or

    print"Here are the curly braces: {unicode(123)} and {unicode(125)}"

    Dot methods on a String

    Note: There are no ‘substring’ methods in Elan because you can always use an index range get a substring e.g. s[3..7] gives a string containing the fourth through the eighth characters of string s. See Indexed Value.

    upperCase() returns String

    returns a new string based on the input with all alpha-characters in upper-case.

    lowerCase() returns String

    returns a new string based on the input with all alpha-characters in lower-case.

    contains(partString as String) returns Boolean

    takes a single parameter of Type String, and returns a Boolean value indicating whether or not that argument string is contained within the string on which contains was called. Usage:


    variable a set to "Hello World!"
    print a.contains("ello")

    prints true

    replace(match as String, replacement as String) returns String

    returns a new string where all occurrences of the match string are replaced with the replacement string.

    trim() returns String

    returns a new string based on the string on which the method is called, but with all leading and trailing spaces removed.

    indexOf(partString as String) returns Int

    The following methods are used for comparing strings alphabetically, for example in a sort routine:

    isBefore(otherString as String) returns Boolean
    isAfter(otherString as String) returns Boolean
    isBeforeOrSameAs(otherString as String) returns Boolean
    isAfterOrSameAs(otherString as String) returns Boolean
    asUnicode() returns Int

    returns the Unicode (integer) value for a character. If the string is more than one character long, the Unicode value returned is that for the first character in the string only. Note that the opposite method to create a single-character string from its numeric Unicode value is e.g. unicode(123) which returns "{".

    Arrays and Lists

    Quick reference

    ArrayList
    Type formArray<of String>
    2D: Array2D<of String>
    List<of String>
    Literal["plum", "pear"]{"plum", "pear"}
    Literal emptyempty Array<of String>empty List<of String>
    Initial size (filled with default values)variable a set to createArray(10, "x")
    variable a set to createArray2D(8, 8, "")

    In each case, the last argument is the value to which each element
    is initialised, and defines the Type of elements in the Array

    Not applicable
    Read from positiona[3]
    board[3][4]
    a{3}
    Read rangea[5..9]a{5..9}
    Put a valuecall a.putAt(3, "pear")
    2D: call board.putAt2D(3,4,"K")
    set a to a.withPutAt(3, "pear")
    Append and prependcall a.append("pear")
    call a.prepend("pear")
    call a.appendArray(anotherArray)
    call a.prependArray(anotherArray)

    Append one item: set a to a + {"pear"}
    Prepend one item: set a to {"pear"} + a
    Append or prepend a List: set x to listA + listb
    Insertcall a.insertAt(3, "pear")set a to a.withInsertAt(3, "pear")
    Remove by indexcall a.removeAt(3)set a to a.withRemoveAt(3)
    Remove by valuecall a.removeFirst("pear")
    call a.removeAll("pear")
    set a to a.withRemoveFirst ("pear")
    set a to a.withRemoveAll ("pear")
    Deconstruction into head (first element)
    and tail (all the rest)
    Not applicablevariable x:xs set to myList
    set h:t to myList

    To discard either the head or the tail:
    variable _:tail set to myList
    variable head:_ set to myList

    Array

    An ‘Array’ is a simple data structure that holds multiple elements of the same Type.

    Unlike a List, an Array is mutable, meaning that the elements within the data structure can be altered without creating a new Array from the old.

    The Type is specified in the following form:

    Array<of String> for an Array of Type >el-code>String
    Array<of Int> for an Array of Type Int

    where, in these examples, String and Int represent the Type of each element in the Arrays. The element Type can be any Type value – Int, Boolean, Float, String – or the name of a specific class such as Player, or an enum such as Direction. It may also be another data structure, including another Array (sometimes referred to as a ‘nested Array’).

    Creating an Array

    An Array may be defined in ‘literal’ form, ‘delimited’ by square brackets, and with all the required elements separated by commas. The elements may be literal values but must all be of the same Type:

    variable fruit set to ["apple", "orange", "pear"]

    including ‘nested ’:

    variable coordinates set to [[3.4, 0.1, 7.8], [1, 0, 1.5], [10, -1.5, 25]]

    or variables (provided they are all of the same Type):

    variable values set to [x, y, z]

    or a mixture of literal values and variables (all of the same Type):

    variable values set to [3.1, y, z]

    where y and z are existing variables of Type Float.

    You may also define an Array of a specified size, with each element initialised to the same value, for example:

    variable fruit set to createArray(20, "")

    creates an Array of Type String with exactly 20 elements, each initialised to the empty String and:

    variable scores set to createArray(12, 100.0)

    creates an Array of Type Float with exactly 12 elements, each initialised to 100.0.

    Although the resulting Array may still be expanded subsequently (by using the add procedure), the typical use for these two methods is for cases that would originally have used a traditional (fixed-size) array.

    Dot methods on an Array

    Functions:

    myArray.contains(item) returnss true or false

    myArray.asList() returnss a List containing the same elements as the Array on which the method was called. This is often used to permit an Array to be passed into a function that has been designed to accept a List.

    Procedures:

    call fruit.append("banana")
    call fruit.appendArray(anotherArray)
    call fruit.insertAt(4, "cherry")
    call fruit.prepend("melon")
    call fruit.putAt(2, "grape")
    call fruit.removeAll("apple")
    call fruit.removeAt(3)
    call fruit.removeFirst("apple")

    Using an Array

    Elements are read using an index in square brackets, the first element being element [0]. The last element of an Array of size 10 will therefore be accessed by the index [9].

    Attempting to read an element by index, where that element does not exist, will result in an ‘Index out of range’ run-time error.

    Unlike in many programming languages you may not modify data by index: elements are modified by calling the putAt procedure on the Array.

    Try these examples (the last one will produce an error – make sure you understand why):

    variable a set to createArray(10, 0)
    print a
    print a.length()
    call a.putAt(0, 3)
    call a.putAt(1, 7)
    print a
    print a[0]
    print a[a.length() -1]
    print a[a.length()]

    Unlike in some languages, Elan Arrays may be dynamically extended, using append and prepend methods.

    variable a set to createArray(3, 0)
    variable b set to createArray(3, 10)
    print a
    print b
    a.append(3)
    b.prepend(7)
    print a
    print b
    a.appendArray(b)
    print a

    2-dimensional Array

    In Elan, as in many languages, a ‘2-dimensional Array’ is just an Array of Arrays. However, Elan provides a couple of convenient short-cut methods for working with such data structures:

    variable board set to createArray2D(8, 8, "")
    will create an Array of Arrays with a total of 64 elements each of Type String, and initialised to the empty String. The Type is determined by the Type of the third parameter, which might be an Int, Boolean, or user-defined Type. It need not be an empty value. The Array2D need not be square – it may be rectangular.
    It is possible to create an 2-dimensional Array with no elements, for example by:

    let a be new Array2D<of Int>()

    However, this is not recommended as subsequently adding elements takes a lot of care and effort. It is recommended that you always use the method createArray2D to create a 2-dimensional Array initialised to the desired size. That way you can modify individual elements in the initialised Array with e.g.:

    call board.putAt2D(3,4,"K")

    and you can read individual elements with a double index, for example:

    for col from 0 to 7 step 1
    for row from 0 to 7 step 1
    print board[col][row]
    end for
    end for

    If you want to define a function or procedure with a parameter that should be a 2-dimensional Array, the Type is specified as Array2D, for example:

    Array2D<of String>
    Array2D<of Int>

    Creating Arrays of specific sizes

    The following methods return an Array of a specified size, and with all elements initialised to a specified value. Although the resulting Array may still be expanded subsequently (by using the add procedure), the typical use for these two methods is for cases that would originally have used a traditional (fixed-size) array:

    createArray(size as Int, initialValue as Type) returns [Type]

    where Type is one of the following Types: Int, Float, Boolean, String or any Type of enum.

    There is also a variant of the method that creates a ‘2-dimensional’ rectangular Array (actually an Array of Arrays):

    createArray2D(noOfrows as Int, noOfColumns as Int, initialValue as T) returns [[Type]]

    List

    A List is a simple data structure that holds multiple elements of the same Type.

    A list, like a String but unlike an Array, is immutable. You can still insert, delete, or change elements in a List, but the methods for these operations do not modify the input List: they return a new List based on the input List but with the specified differences.

    Type name

    The Type is specified in the following way:

    List<of String> for a List of TypeString
    List<of Int> for a List of Type >Int
    List<of List<of Int>> for a List of Lists of Type Int

    Creating a List

    A List may be defined in ‘literal’ form, ‘delimited’ by curly braces, and with all the required elements separated by commas. The elements may be literal values but must all be of the same Type):

    variable fruit set to {"apple", "orange", "pear"}

    Dot methods on a List

    The dot methods on a list are all functions.

    myList.contains(item) returns true or false
    myList.asArray() returns a new Array with the same contents as myList

    The following functions all return a new List, copied from the List on which the function was called, but with the differences specified by the function parameters:

    myList.withInsertAt(4, "cherry")
    myList.withPutAt(2, "grape")
    myList.withRemoveAt(3)
    myList.withRemoveFirst("apple")
    myList.withRemoveAll("apple")

    Try these examples:

    variable fruit set to empty List<of String>
    print fruit
    set fruit to fruit + "apple"
    set fruit to fruit + "pear"
    print fruit
    set fruit to "orange" + "pear"
    print fruit[0]
    print fruit.length()
    print fruit[fruit.length() -1]
    variable head:tail set to fruit
    print head
    print tail

    Dictionaries

    There are two forms of dictionary in Elan: an ordinary Dictionary (which is mutable) and a DictionaryImmutable (which is not).

    Quick reference

    DictionaryDictionaryImmutable
    Type formDictionary<of String, Int>DictionaryImmutable<of String, Int>
    Literal["a":1, "b":4]{"a":1, "b":4}
    Literal empty empty Dictionary<of String, Int>empty DictionaryImmutable<of String, Int>
    Read the value for a given keyd["a"]d{"a"}
    Get all keys
    Get all values
    d.keys()and d.values()
    both return an immutable list of the appropriate Type
    Define (or change) a value associated with a keycall d.putAtKey("c", 7)set d to d.withPutAtKey("c", 7)
    Remove both key and valuecall d.removeAtKey("c")set d to d.withRemoveAtKey("c")

    Dictionary

    Type name

    In the following example, the keys are of Type Int, and the values associated with the keys are of Type String:

    Dictionary<of String, Int>

    For both Dictionary and DictionaryImmutable the values can be of any Type, including a specific Type of class, a List, another Dictionary or some other data structure. However, the key’s Type must be one of: Int, Float, String, Boolean, or a specific Type of enum.

    Defining a literal

    A literal Dictionary is defined as a comma-separated list of ‘key:value pairs’ (key,colon.value) surrounded by square brackets:

    variable scrabbleValues set to ["a":1, "b":3, "c":3, "d":2]

    Using a Dictionary

    Try these examples:

    variable dict set to new Dictionary<of String, Int>()
    print dict
    call dict.putAtKey("a", 3)
    print dict["a"]
    call dict.removeAtKey("a")
    print dict

    Constraints

    Key values must be unique within a Dictionary.

    There is no difference in syntax between adding an entry with a new key, and setting a new value for an existing key: if the key does not exist in the dictionary, it will be added.

    Dot methods on a Dictionary

    See also Quick reference.

    putAtKey
    removeAtKey
    keys
    values

    DictionaryImmutable

    An immutable dictionary may be defined in a constant.

    Type name

    In the following example, the keys are of Type Int, and the values associated with the keys are of Type String:

    DictionaryImmutable<of String, Int>

    Defining a literal

    A literal DictionaryImmutable is defined as a comma-separated list of ‘key:value pairs’ (key,colon.value) surrounded by curly braces:

    variable scrabbleValues set to {"a":1, "b":3, "c":3, "d":2}

    Using an Immutable Dictionary

    Try these examples:

    variable immD set to new DictionaryImmutable<of String,Int>()
    print immD
    set immD to immD.withPutAtKey("a", 3)
    print immD["a"]
    set immD to immD.withRemoveAtKey("a")
    print immD

    Dot methods on an Immutable Dictionary

    See also Quick reference.

    hasKey
    withPutAtKey
    withRemoveAtKey

    Tuple

    A tuple is a way of holding a small number of values of different Types together as a single reference. They are referred to as 2-tuples, 3-tuples, etc. according to the number of values they hold. Common uses include:

    Holding a pair of x and y coordinates (each of Type Float) as a single unit, and

    Allowing a function to pass back a result comprised of both a message in a String and a Boolean indicating whether the operation was successful.

    A tuple is considered a ‘lightweight’ alternative to defining a specific class for some purposes.

    Type name

    Written as a comma-separated list of the Type of each member, surrounded by round brackets:

    (Int, Int, Int)
    (String, Boolean)

    Defining a literal tuple

    A tuple is defined, where it is needed, by the keyword tuple and a number of elements, each being a variable or a literal value, separated by commas and surrounded by round brackets, for example:


    let coords be tuple(x, y)
    let myList be {tuple(1,1), tuple(1, 2)}
    let foo be tuple(3.769, 4.088, true, 5, "correct")
    call proc1(a, coords, tuple(x, y))
    return tuple(3, 4)

    Using a tuple

    You may pass a tuple into a function, or return one from a function, for example:

    variable d set to distanceBetween(point1, tuple(12.34, 20.0))

    An existing tuple (for example point1 below) may be ‘deconstructed’ into new variables or named values (where the number of variables/names must match the number of elements in the tuple):

    let x, y set to point1
    variable x, y set to point1


    or into existing variables of the correct Type:

    variable a set to 3
    variable b set to 4
    set a, b to point1

    The ‘discard’ symbol _ (underscore) may also be used when deconstructing a tuple when there is no need to capture specific elements:

    variable x, _ set to point1

    Notes

    As in most languages, Elan tuples are immutable. Once defined they are effectively ‘read only’. You cannot alter any of the elements in a tuple nor (unpke a pst for example) can you create a new tuple from an existing one with specified differences.

    You cannot deconstruct a tuple into a mixture of new and existing variables.

    tuples may be nested: you can define a tuple within a tuple.

    Func

    A function may be passed as an argument into another function (or a procedure), or returned as the result of calling another function. This pattern is known as ‘Higher order Function’ (HoF), and is a key idea in the functional programming paradigm. To define a function that takes in another function as a parameter, or returns a function, you need to specify the Type of the function, just as you would specify the Type of every parameter and the return Type for the function.

    Type name

    The Type of any function starts with the word Func followed by angle brackets defining the Type of each parameter, and the return Type for that function, following this syntax:

    Func<of String, String, Int => Boolean>

    This example defines the Type for a function that defines three parameters of Type String, String, and Int respectively, and returns a Boolean value. T this Type would match that of a function definition that started:

    Function charactersMatchAt(a as String, b as String, position as Int) returns Boolean

    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, Array, List, 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]
    print a[4..]
    print a[..5]
    print a[0..4]

    See also: Using an Array, 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:

    putAt on an Array
    withPutAt on a List
    putAtKey on a Dictionary
    withPutAtKey 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’)

    Note: 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 notandor.

    Example that implements an ‘exclusive or’.


    function xor(a as Boolean, b as Boolean) returns Boolean
      return a and not b or b and not a
    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)
    (a + b) > (c -d)

    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)
    variable x set to sinDeg(30)^2 + cosDeg(30)^2
    variable name set to inputString("Your name")
    print name.upperCase()

    Notes

  • The third example above is not strictly a function call, but is a ‘system method’Error! Reference source not found. call. System methods may only be used within the Main routine or a Functions and 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:

  • If the functionality it defines is needed in only one location; typically for a particular call to a HoF.
  • If you need to capture a local variable in the implementation. This is called ‘closing around a variable’.
  • The syntax for a lambda is as follows:

  • Start with the keyword lambda.
  • Parameter definitions, comma-separated, follow the same form as parameter definitions in a function or procedure, but with out surrounding brackets.
  • The => symbol, which is usually articulated as ‘returns’, ‘yields’ or even ‘fat arrow’.
  • An expression that makes use of the parameters, and may also make use of other variables that are in scope.
  • Example:


    function liveNeighbours(cells as [Boolean], c as Int) returns Int
      let neighbours be neighbourCells(c)
      let live be neighbours.filter(lambda i as Int => cells[i])
      return live.length()
    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.
  • 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 safeSquareRoot(x as Float) returns Float
      let root be lambda => sqrt(x)
      return if x < 0 then 0 else root()
    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 – 1160
    return if isGreen(attempt, target, n) then setChar(attempt, n, "*")
    else attempt
    return if attempt[n] is "*" then attempt
    else if isYellow(attempt, target, n) then setChar(attempt, n, "+")
    else setChar(attempt, n, "_"))