RPN Calculator in Dylan

The following implementation makes use of the functional style favoured by Dylan, as well as its exception handling capabilities. Exceptions are used both for catching errors and for control structuring.

The program is presented as a module – this is a requirement in Dylan. It also needs two ‘configuration’ files to specify what library modules are imported in order for this one to work, and to distinguish it as the ‘main’ module, in the sense that the entry point of the module is the entry point of the program.

The main part of the program is the short block at the very end of the text. It does nothing but call the method (function) rpn. rpn reads an input line, splits it up into a sequence of tokens, calls process-line to evaluate the expression, and finally calls itself again to handle the next line. This is a tail-recursion, however, and the Dylan compiler implements it as a loop.

Upon encountering end of input, read-line raises a corresponding exception, which is catched within the main block, and the program terminates. In general, blocks of the form block()-exception()-end are for catching and handling exceptions. (They also have some other functions, not used here.) The part before exception, and whatever is called from there, is the code guarded against exceptions. Next follows the handler, preceded by the name of an exceptions class – the one serviced by the handler.

The function process-line is largely composed of another such block. It calls the library function reduce to traverse the sequence of tokens and evaluate the expression. reduce, in turn, does its work by repeatedly calling the method process-term, local within process-line, which checks whether the currently seen token is an operation or a number, and, accordingly, takes different actions. If none is the case, or if an operation cannot be performed for lack of operands stored, an exception is raised.

Note how the check for whether a token is an operation is done. It comprises of two actions within a block. The first one asserts that the length of the token is 1. The second uses the token as a key to access the global table $ops, which maps the operation names to the operations themselves. Upon success, the corresponding operation is returned as the value of the block. If anything goes wrong, i.e. the token length is not 1 or the token is not a valid key for the table, an exception is raised. In that case, the handler ensures that the value the block returns is #f. Eventually, the value is assigned to op and checked in the if expression that follows closely.

If the token was an operation name, and thus op is that operation, two values are popped off r, and the operation is performed on them. If it is #f, signifying ‘false’, we hope the token holds a number and try to obtain the latter by calling string-to-float. The assert that follows checks if the whole token was used to obtain the number, i.e. if the token was indeed a number.

Finally, the if produces a result that is pushed, by add!, onto r. But if the assert or a pop was unsuccessful, an exception is raised, so that add!, process-term, and reduce are all abandoned and the enclosing block is exited after printing an error message.

It remains to add that reduce starts its working by make-ing a deque object, to serve as the stack r within process-term. (Dylan does not have stacks per se; the closest suitable kind of collection available in the language is the more general deque.) Each time a token is passed to process-term, the stack is modified and returned as the value of the method (as it is the value returned by add!), and thus it is, again, the value of r for the next call of the method. When reduce ends, it returns that same stack, which then must contain only one entry – the value of the expression.

force-output is needed because the output of the program is normally deferred due to buffering, while we want it properly alternated with the input.

module: rpn

define function rpn()
  let (#rest s) = split("[ \t]",read-line(*standard-input*));
  if (~s.empty?)  process-line(s);  end;

define function process-line(s)
    local method process-term(r,s)
      let op = block()
                 assert(1 = s.size);
               exception(<condition>) #f;  end;
      add!(r, if (op)
                let (z,y) = values(r.pop,r.pop);
                let (x,i) = string-to-float(s);
                assert(i = s.size);
    let r = reduce(process-term,make(<deque>),s);
    assert(1 = r.size);
  exception(<condition>) format-out("error\n");  end;

define table $ops =
  {'+' => \+, '-' => \-, '*' => \*, '/' => \/};

exception(<end-of-stream-error>)  end;