Friday, 24 February 2012

Computing in Libretto (4 of 5). Exception Handling

Exceptions and their handling in Libretto are considered here. Please pay your attention to local traps.


How to Throw an Exception

The error function is used for throwing exceptions. It can be called with one or two arguments:
error(exc: Any)
error(exc: Any, value: Any) 
The first argument is the object of the thrown exception. Any value can play the role of this object. The optional second argument serves as the context value for the exception handler. If the second argument is omitted, then the same context value is used for the handler as for the expression, in which the exception occurs.

The invoked error function stops the computation of the current expression and hands over the control to the nearest exception trap, whose condition matches the exception object (the first argument of error).

Libretto supports two types of traps:

  • the try operator intended for exception handling in large code segments;
  • a local trap intended for on-the-spot exception handling.

Try expressions are familiar to many programmers. Local traps work in paths. They provide a lightweight handling, when exceptions are processed locally.

Operator try and Global Exception Handling

The behavior of the try operator in Libretto is like that in Java and Scala:
(1, -1, 2, 3). 
  try {
    if ($ < 0) error('negative) 
    $ * 1000
  } 
  catch {case 'negative => -$} 
  finally {$ + 1}

  //  1001 2 2001 3001
A try expression has the form:
try {try-block} catch {catch-block} finally {finally-block}
The ‘catch’ and ‘finally’ blocks are optional. The catch-block has the form of a case expression (a partial function).

A try expression has the following semantics:

  1. It is executed for each element of the context sequence.
  2. First the try-block is executed for the current context value.
  3. If an exception is thrown, then the catch-block is executed, which tries to catch and handle the exception.
  4. If the condition of some case expression successfully matches with the exception object, then the right-hand side of this case expression is executed in the context of the second argument of error (if it is binary), or in the same context as for the try-block (if error is unary). Its result is interpreted as the result of the catch-block.
  5. If a try expression contains a finally-block, then it is executed in the context of either the result of the try-block (if it has been finished), or the catch-block (if an exception has been thrown). The result of the whole try expression for the current context value is the result of the finally-block.
  6. If there is no finally-block, then the value of the try expression for the current context value is the value of the try-block (if its computation has been successful), or the catch-block (if an exception has been thrown).
  7. If a catch-block does not manage to catch an exception object, then the exception is thrown out.
  8. In paths, a try expression is part of a step.

Here is an example of a binary error application:
(1,2,0,4). 
  try {if ($ == 0) error(“div by 0”, “oo”) else 100 div $} 
  catch {case “div by 0” => $ + “ps”}

// 100 50 “oops” 25
Unary error is expressible in terms of binary one:
expr. try {... error(e) ...} ...
is equivalent to
expr as x. try {... error(e, x) ...} ...
Finally-blocks in nested try expressions are computed in the order they occur. If an exception is thrown, the most nested matching catch is executed. The catch-block and finally-block are optional and can be omitted (but not both).

If a try expression occurs in a path step, then it must be enclosed in parentheses, except if it is the rightmost in a path. Then the parentheses can be omitted (as in the examples above).

Local Exception Handling

The notation of try expressions is heavy. Sometimes such heaviness has its reasons (when large code segments are involved, and exceptions are handled globally). But in many cases it is much better to handle exceptions locally on the spot. Such situations are quite frequent (especially when duck-typing is involved), therefore local exception-handling tools are added in Libretto.

A local exception trap is an expression of the form
expr ?{partial-function}
This local trap is sensitive to the expression expr on the left-hand side of the question mark ?. Only exceptions thrown from this expression are caught by this trap. The next function divides 100 by the context value:
def div100 = if (this == 0) error(“div by 0”, “oops”) else 100 div this
If the context value is 0, div100 throws an exception:
(1,2,0).div100  //  ERROR: div by 0
Since the exception is never caught, it stops the computation. Let us revise the expression:
(1,2,0). div100 ?{case “div by 0” => ($, 0)}  //  100 50 “oops” 0 
A local trap behaves as follows:

  1. When an exception is thrown, the local trap tries to match the exception object against its case patterns.
  2. If matching is successful for some case pattern, then the handler in the right-hand side of this case pattern is computed with the context value determined by the second argument of the function error.
  3. If the second argument of error is omitted, then the computation is performed in the same context as for the expression expr, in which the exception occurs.
  4. The value of the whole expression is equal to the value of the handler.
  5. If the exception object can not be caught, the exception is thrown out.

The local trap can hunt only for exceptions thrown from the nearest expression:
(1,2,0). div100. ($ + 5) ?{case “div by 0” => ($, 0)}
  //  ERROR: div by 0
Here the local trap is tuned on the expression ($ + 5), but the problem occurs in div100. This exception cannot be caught. In such cases we can use the try operator, or put the expression in parentheses or curly brackets:
(1,2,0).(div100.($ + 5)) ?{case “div by 0” => ($, 0)}
  // 105 55 “oops” 0
A trap set after a block operator can catch exceptions thrown from any expression of this block:
{var x = (1,2,0).div100; x = x + 5} ?{case “div by 0” => $}
  //  “oops”
Here the trap has caught an exception from the first assignment of the block.

Immutable structures are good as exception objects, because they are powered by pattern matching:
fix class ColorErr(msg: String)
fix class ValErr(msg: String)

class Color(clr: Any) {
  fix color: String  
  clr match {
    case c if c == (“red”, “green”, “blue”) => color = c
    case x if String => error(ColorErr(x))
    case x => error(ValErr(x))
  }
}

def createColor(x) = 
  (Color(x).color)
     ?{case ColorErr(x) => x + “!”; case ValErr(x) => x +“?”}

createColor(“red”)  //  “red”
createColor(“Ann”)  //  “Ann!”
createColor(123)  //  “123?”
Since exception handlers are partial functions, regular pattern matching works in them.

The following trap catches all exceptions:
expr?{case _ => handling any exception}
Please remember that disabling all exceptions at once is not a good programming style.

Local traps are helpful for work in the EAFP style (“it is Easier to Ask for Forgiveness than for Permission”), which prefers the control via exceptions over the control via if checks. This approach is closely related to duck typing and is a popular programming style in Python. For instance, the expression using the if operator for checks
var ham = spam. if (hasField('eggs)) eggs else handleError()
in the EAFP style looks as follows:
var ham = spam.eggs ?{case “no field” => handleError()}
The hasField function checks if a certain field is defined on the context object.

The Empty Context as the Weakest Exception

Sometimes we need to prevent backtracking caused by the empty sequence in paths. For instance, the expression
(1, -1, 2)?[$ > 0]  //  1 2
returns () for the value -1, because the predicate does not let it through. The empty sequence launches backtracking and selecting the next context value (2 in this case). A special trap expr?(...), which catches empty sequences, allows us to avoid such situations:
(1, -1, 2) as x ?[$ > 0] ?(-x * 100)  //  1 100 2
The value in parentheses substitutes the empty context.

Deep Backtracking with jump

The error function has a twin function jump. Its behavior is almost similar to that of error (with the exception of transactions, see section Multitasking), but the use of jump indicates that the normal exit is performed, rather than an error occurs. Consider the following definitions:
fix class Person(name: String, surname: String, hasChild: Person*)

def Person findDescendant(name: String) {
  fix ch = hasChild?[name == @name]
  if (ch) ch
  else hasChild.findDescendant(name) 
}

object JOHN extends Person(“John”, “Smith”, 
                       Person(“Paul”, “Smith”, Person(“Ann”, “Smith”)),
                       Person(“Ann”, “Thomas”)) 

JOHN.findDescendant(“Ann”).surname  //  “Smith”  “Thomas”
The findDescendant function enumerates person’s descendants and selects those of them who have a specified name. But if we need to check whether there exists such a descendant, the complete enumeration is needless, because it is sufficient to find the first of them. By using jump this problem can be solved:
def findDescendant(name: String) {
  fix ch = hasChild ?[@name == name]
  if (ch) jump(ch)
  else hasChild.findDescendant(name) 
}

JOHN.findDescendant(“Ann”) ?{case ch if Person => ch.surname}
  //  “Smith”
The first found Ann induces the jump, which throws the object of Ann as the result. In the caller this object is caught. If no offspring found, the result is the empty sequence.

No comments:

Post a Comment