RPN Calculator in Pure

The function definitions in this implementation of the calculator look like ones in most modern functional languages. These definitions are particular cases of Pure's rewriting rules, but the greater generality of the language in this respect is not made use of here.

The rpn function is the main one, and so is called at the very end of the program. The first thing that it does is checking the guard condition ~(feof stdin) which ensures that the standard input is not exhausted. Then the when part is computed which provides a local value s for rpn's main expression. s is obtained by reading (gets) a line of text, breaking it down to substrings, of which the RPN expression tokens are to be extracted, and filtering out the empty ones among them. rpn finally passes s to process and calls itself tail-recursively. ($$ is a sequencing operator throwing away the result of its left argument, which is anyway of no significance here.)

The process function is a composition of several parts applied in a right-to-left order ($ being the function application written explicitly as a right-associative infix operator). In this ‘pipeline’, map xtok translates all strings to numbers or operators: xtok translates a single member of the sequence s, and map replicates xtok's action over each item of s. An empty list is attached to the resulting token sequence, and the so constructed list pair is passed to evalrpn for computing the RPN expression in the first list. Next in the pipeline is the call of the catch special form with an error handling function which, ignoring its argument – the actual error – just returns the error symbol. Finally, whatever catch returns – a resulting number or the error symbol – is transformed (str) into a string which puts prints out to the standard output.

Two things should be stressed about how process works. One is that all it does is guarded by a condition ensuring that only non-empty sequences are processed. The other is that the call of map xtok is nested within that of evalrpn, which in turn takes place within the call of catch. As both xtok and evalrpn can throw exceptions but each of these functions is called witin the catch, any such exception is properly caught.

For token recognition xtok makes use of regular expressions: nu for numbers and op for operators. Matching is done by a call of regex wrapped within the help function mtk, in order to vary the pattern being applied without repeating the actual, rather bulky, regex calling sequence.

A token representing an arithmetic operator is mapped to the respective function through looking it up in the associative table fn. Obtaining the value of a token that reads as a number is a bit more complicated. For this, in principle, we use the built-in function val which is designed to evaluate any common kind of expression written as a string. However, as numbers in Pure cannot begin with a +, neither is + a valid monadic operator, and also because Pure does not accept numbers ending with a decimal dot, these peculiarities, if present, are removed from the processed string before passing it to val. This is achieved by wrapping val within the num function and calling the latter in place of the former.

A token parsing as neither a number or an operator is handled by throwing an exception; the value passed (the empty tuple ()) is nowhere used and because of that immaterial.

One can note that the definition of xtok makes use of with and when clauses, both for local definitions: Pure requires using the former for defining only functions, and the latter for defining other kinds of values. More generally, with is for term transforming equations, while when is for name binding through pattern matching. In certain cases an equation may be syntactically indistinguishable from a matcher, hence the need for superimposing a disambiguating structure.

evalrpn traverses the token sequence which is the first in the pair of lists that is its argument, maintaining a stack in the second list. Numbers from the first list move to the stack. Operators apply on the two topmost numbers from the stack, and the result (t x y) gets pushed on their place. When the expression sequence gets empty, the stack should contain a single number n; if there are more, an exception is thrown. An exception is also raised implicitly if ns fails to match the pattern y:x:rs that pops the arguments for applying an operator.

Typically of Pure, four kinds of functions and function-like constructs are used in this program. The functions head, tail, init, last, map, and filter are from the standard library, as are stdin, feof, gets, puts, regex, and regsplit. The first ones are all members of the prelude module, whose content is made available to the program automatically. The second group is from the system module which is loaded explicitly through a using clause. There are also built-in primitives – val, str, realp, and throw in this program – and special forms, such as if–then–else and catch. To each special form there corresponds a specific rule for its computation. As a rule, special forms cannot be expressed as ordinary functions.

using system;

rpn = process s $$ rpn if ~(feof stdin)
  when s = filter (~= "") (regsplit "[ \t]" 0 gets 0) end;

process s = puts $ str $ catch (\_ -> error) $ evalrpn $ (,[]) $ map xtok s
  if s ~= [];

xtok tk = if mtk nu then num tk else if mtk op then fn!tk else throw ()
  with mtk p = regex p (REG_EXTENDED or REG_NOSUB) tk 0;
       num t = val (if last t1 == "." then init t1 else t1)
         when t1 = if head t == "+" then tail t else t end end
  when nu = "^[+-]?(\\.[0-9]+|[0-9]+(\\.[0-9]*)?)$";
       op = "^[-+*/]$";
       fn = {"+" => (+), "-" => (-), "*" => (*), "/" => (/)} end;

evalrpn (t:ts,ns) = evalrpn (ts, (if realp t then t:ns
                                  else (t x y : rs when y:x:rs = ns end)));
evalrpn ([],n:ns) = n if ns == []; = throw ();