Evlan
Expressions
Last updated at 2005/04/23 14:34:33 PDT by Temporal

Evlan is a functional language, and thus is composed primarily of expressions. This section describes those expressions used in pure functional code. Expressions used with imperative tasks are described in another section.

Arithmetic

Evlan, like most languages, supports many standard arithmetic operators.

Binary operators are used by placing the operator between the parameters. These include the following, in decreasing order of precedence:

  • ^ The power operator.

  • * / % Multiply, divide, and modulus operators. Note that division of two integers could result in a non-integer. Integer division can be emulated by calling Scalar.floor() on the result. Note that modulus can be applied to any real numbers, not just integers.

  • + - Add and subtract operators. Adding a number to or subtracting a number from a character will result in another character, but characters are not otherwise numeric.

  • == != < <= > >= Comparison operators. Numbers, data, characters, and atoms can be compared with these operators. Note that the ordering of atoms has nothing to do with their names and may be different in different instances of the virtual machine. (Atoms will always have the same ordering in a particular persistent state, though.)

  • and or Boolean operators. Note that "and" will return false if either parameter is false, even if the other parameter is not even a boolean. Similarly, "or" will return true if either parameter is true.

  • :: The cons operator. This is a shortcut for the List.make() function. Also, the variable "empty" is defined by default to be an empty list. Thus, you can construct a list containing the elements 1, 2, 3, and 4 as follows:

    1::2::3::4::empty

There are also two unary operators in Evlan. These are applied by placing them before an expression. Unary operators currently have higher precedence than all binary operators, which is unfortunate as this is mathematically incorrect in the case of the power operator. The operators are:

  • - Unary minus. Negates the parameter.

  • not Boolean not.

Variables

Variables are identified by identifiers (fancy that). They are defined using other expressions, such as abstractions or where blocks, and are usable within a certain scope.

If multiple variables with the same name are defined in a particular scope, the innermost definition is used (i.e. the definition with the smallest scope). However, in Evlan, you may frequently find yourself wanting to refer to a variable other than the innermost one. To do this, you may prefix the variable name with one or more \s (backslashes). A backslash means to skip the innermost variable and use the next innermost one; multiple backslashes let you skip further out.

As an example of a time when you might want to use this, imagine a constructor for a 3D vector. The members of the vector are named x, y, and z. The parameters to the constructor should also logically be named x, y, and z. However, the following code won't work:

(x, y, z) => {x = x, y = y, z = z}

Here, x, y, and z are being defined equal to themselves, and the parameters to the function are being ignored. There are two ways you could fix this:

(x2, y2, z2) => {x = x2, y = y2, z = z2}
(x, y, z) => {x = \x, y = \y, z = \z}

Clearly the latter solution is "prettier", though in this case either solution would do. In some cases, however, you will find it very hard to avoid using backslashes without making your code much uglier.

That said, don't overuse backslashes. Ideally they should only be used as in the example above, when you want to define a member of an object to be equal to a variable of the same name. Especially avoid using more than one backslash at a time.

Conditionals

Conditionals take the form:

if condition then expression1 else expression2

"condition" must be an expression which evaluates to a boolean. If it is true, the result of the conditional is the result of evaluating expression1. Otherwise, it is the result of evaluating expression2.

Unlike in imperative languages, you cannot omit the "else" part of a conditional, nor would it make sense to do so anyway.

Where Blocks

Where blocks take the form:

resultExpression where
   var1 = expression1
   var2 = expression2
   var2 = expression3
   ...

Here, the variables var1, var2, and var3 will be defined within the expressions expression1, expression2, expression3, and resultExpression. Each variable is defined equal to the result of evaluating the expression to the right of the equals sign. The result of the where block is the result of evaluating resultExpression, given the other definitions.

For example:

x + y where
   x = y * 10
   y = 12

The above code evaluates to the number 132.

Note that the variables within a where block may be defined in any order, and may refer to each other freely. It is even possible, in some cases, for the definitions of two variables to refer to each other. For example:

x where
   x = {a = y}
   y = {b = x}

This defines two objects x and y each containing one member, where those members refer to each other. Thus, "x.a.b" is equivalent to "x".

In practice, such cyclic references are used mainly in defining functions which call each other (possibly recursively).

Arrays

The following two code fragments evaluate to an array containing elements 1, 'a', 9, and @hello:

{1, 'a', 3*3, @hello}
array of
   1
   'a'
   3*3
   @hello

Each element of an array can be any type of value. Retrieve the ith elements of an array as follows:

myArray[i]

The elements of an array are numbered starting from zero.

The size of an array can be queried as follows:

myArray.size

You can also create arrays dynamically via function calls in the Evlan API, documented elsewhere.

Objects

An object is a value which contains a set of named fields. Create one like so:

object of
   field1 = expression1
   field2 = expression2
   ...

Alternatively, you can use the braced form, which is sometimes more convenient:

{field1 = expression1, field2 = expression2, ...}

This defines an object with fields field1 and field2, which are equal to the results of evaluating expression1 and expression2. Each member field of an object is defined within the scope of the object. This means that the definition of one member may use another member. Like with where blocks, fields may be defined in any order, and multiple fields can even reference each other cyclically as long as those references make sense.

Members of an object are accessed like so:

myObject.memberName

Functions

Since Evlan is a functional language, functions are treated just like any other value. You can pass functions as parameters to other functions, for example. Also, you need not name a function when defining it. Here is a definition of a function which takes one parameter and returns the square of that parameter:

x => x * x

The "=>" symbol is called the "abstraction operator". Function definitions are also known as "abstractions", because they are abstracting an expression over all possible values of the parameters.

You can define a function with multiple parameters like so:

(a, b) => a * b

You can even define functions which take zero parameters. The purpose of such a function is to delay the computation of the result until the function is actually called. The assumption is that the function might not be called, and then the time it would have taken to compute the result would be saved. Future versions of Evlan will implement lazy computation to make zero-parameter functions unnecessary, but as of version 0.3 this work has not yet been done.

Note that the definition of a function is free to refer to variables defined outside the function. It is common practice, for example, to define a function which returns another function, where the returned function uses parameters given to the outer function in its definition.

To define a recursive function, you must give the function a name by, for example, declaring it within a where block. Here is a definition of the Fibonacci function:

f where
   f = i => if i < 2 then i else f(i-1) + f(i-2)

Note that this version is very slow because most numbers in the sequence will end up being computed multiple times. Future versions of Evlan will automatically optimize primitive recursive functions like this one into iterative forms. However, as of version 0.3, you need to do this yourself. You might rewrite Fibonacci as follows:

i => loop(1, 1, 0) where
   loop = (j, current, previous) =>
      if j == i
         then current
         else loop(j+1, current+previous, current)

This form is tail recursive. This means that when the function calls itself, the result of the nested call ends up being the result of the parent call. Such functions can be trivially optimized into iterative loop forms, and Evlan does this. You should try to make all of your recursive functions tail recursive if possible.

Calling a function works like it does in any other language:

myFunction(param1, param2, param3)

Note that you cannot omit the parentheses when there is only one parameter. This differs with some functional languages, but is consistent with mathematical notation. (Evlan does not encourage the use of "curried" functions. Although a neat concept in theory, they tend to be confusing to read and difficult to implement efficiently while offering no practical advantages.)

You can also apply a function like this:

myFunction of
   param1
   param2
   param3

This is equivalent to the previous example.

evlan.org © Copyright 2003-2005 Kenton Varda
Powered by Io Community Manager, Evlan, and FreeBSD