In order to perform operations with side effects in a purely functional language, Evlan programs use "tasks". A task is a value which represents some side-effecting operation. The task itself can be created and passed around within purely functional code, but it cannot be executed. In order to execute a task, it must somehow be sent to the system.
When using the Evlan command client, you can use the "/do" interpreter directive to tell the VM to execute a task. For example:
/do console.write("Hello World!")
This will cause the text "Hello World!" to be sent to the client.
In contrast, if you simply type:
console.write("Hello World!")
you will be informed that the result of the expression was a task. In this case, the task is not implemented.
You could even do this:
myTask = console.write("Hello World!")
Now "myTask" is the task to write "Hello World!" to the console. You could then type:
/do myTask
as many times as you want to cause "Hello World!" to be repeatedly printed.
Of course, interpreter directives can only be entered by the user. So how do we write code which executes tasks? Well, first of all, realize that in order for any task to be executed, someone has to do a "/do" in the interpreter somewhere (or use ":=", which is like "/do" but stores the result to a variable). But, we would like to write whole programs that can be executed with a single interpreter directive.
Do Blocks
A "do" block combines a series of tasks which are to be performed in sequence. "do" is a block keyword, and is thus followed by any number of statements. These statements are executed in sequence. Each statement can take one of three forms:
-
Simple assignment: A statement of the form:
var = expression
will define the variable "var" to be equal to the result of evaluating the given expression. No tasks are executed here. The variable may recursively refer to itself in its definition.
-
Task: A statement which is simply an expression will be evaluated and then executed as a task. If the expression does not evaluate to a task, an exception will be thrown.
-
Task with Result: Some tasks return results. To receive the result, use a statement of the form:
var := expression
Here, the expression is again evaluated then executed as a task. The result is stored in a new variable of the given name. Again, if the expression does not evaluate to a task, an exception is thrown. Note that the expression cannot refer to the variable, as the variable does not exist until the task returns. (However, the expression could refer to a previous variable by the same name.)
A "do" block may optionally return a value by issuing a return statement as the last statement in the block. For example:
do x := task1 y := task2 return x + y
The parameter to "return" is a simple expression. It will not be executed as a task. If a return statement is used, it must be the last statement in the block. A return statement cannot be placed within a conditional.
The result of evaluating a "do" block is a task. The statements within the block are not executed until this task itself is executed.
As an example, type this into the command client:
myProc = message => do console.write("Waiting five seconds...") system.clock.wait(5) console.write(message) time := system.clock.getTime() return time
Now execute it:
/do myProc("Hello World!")
Note that since each statement in a "do" block can contain an arbitrary Evlan expression, you can use normal conditional expressions to effect branching:
do ... var := if ... then do ... return ... else do ... return ... ...
However, in imperative code like this, you will often find that you'd like to omit the "else" clause altogether. As a shortcut, you can do this by using an if/do expression:
if condition do ...
Which is equivalent to:
if condition then do ... else Task.doNothing()
Exceptions
When using tasks, Evlan supports exception handling similar to C++ and Java. To throw an exception, use the throw keyword:
throw fileNotFound fileError(filename) error("File not found.")
In this example, a "file not found" error is being thrown. The "throw" keyword is followed by a number of identifiers which identify the type of error being thrown. These are ordered from most specific to least specific. Usually, the last identifier is the generic type "error".
Each identifier may be followed by a number of parameters in parentheses, separated by commas. These parameters give more information about the error. If an identifier requires no parameters, the parentheses may be omitted, as with "fileNotFound" in the above example.
Exceptions can be caught using try and catch:
try doSomething() catch fileNotFound() => handleFileNotFound() catch error(description) => do console.writeError(description) throw catch all() => do console.writeError("Unknown error.") throw finally cleanUp()
Here, "doSomething()" can be replaced with any expression evaluating to a task. This task will be executed as normal, and if no exceptions are thrown, the result will be returned from the "try" task. Each "catch" clause after the "try" specifies what to do if an exception bearing that particular identifier is thrown. Identifiers are checked starting with the most specific (that is, the first one given in the "throw" statement). Thus, the order in which the catches are given is irrelevant.
The identifier in a "catch" clause is followed by a function definition. This function takes, as its parameters, the parameters normally given to that type of exception via the "throw" statement. The result of the function should be a task. When the exception is caught, this task is then executed, and the result of the try/catch becomes the result returned by that task. If the task throws another exception, that exception is thrown out of the try/catch, up to the next one in the call stack.
If, within a catch block, a "throw" statement is given with no identifiers, it means that the exception which was caught should be thrown again.
If a "catch" clause for the special identifier "all" is given, it will be used if no other identifiers match. Otherwise, non-matching exceptions will be thrown past this try/catch and up to the next one in the call stack.
The "finally" clause is followed by a task which should be performed upon completion of the try/catch, whether it completed successfully or resulted in an uncaught exception. The result of this task is ignored, but if it throws an exception, this exception will be thrown out of the try/catch and up to the next one in the call stack.
Please note that the exception system, as specified here, has some known problems. In particular, there is currently no way to investigate the parameters given to an identifier other than the one caught. It is expected that this system will either be modified or replaced in future versions.