RPN Calculator in Oz

Oz being a multi-paradigm language, there are several ways in which the implementation of the RPN calculator can be styled. This particular program exhibits a functional style. It consists of a main part, located near the end, and a procedure, EvalRpns, with two functions nested within it and within each other. The whole program is wrapped up into a functor, which is the compilable unit in Oz.

The main part is responsible for reading the sequence of input lines and passing it to EvalRpns for processing. It is worth noting that the entire input gets read before it is transmitted to EvalRpns, or at least this is how the program text is structured. Since the ReadIn function (locally defined within the main part) is declared lazy, the list of lines that it builds and returns is actually constructed item by item, each line being read and made available no earlier than it is demanded by EvalRpns. The program’s responses are thus properly interleaved with the input. Had the lazy keyword been dropped, the program would have started producing output only after all input has been read and packaged into a list.

There are no direct means in Oz to read the standard input – which is strange, because there are means for printing to the standard output. For reading a line, we use getS – a function that reads arbitrary text files linewise. The said function needs an open file as an argument, which we supply as the value of File. Opening stdin gives rise to some further complications. In order to ensure the appropriate mode of file usage (read, text), we have to instantiate a class, by calling init with corresponding arguments. The class, TxtFile, is defined to inherit from the modules Open.text and Open.file. The former module is needed to provide getS, but it has no init function and so no method of instantiation; init's availability is ensured by inheriting from the latter module.

One may observe not closing the open file, but with stdin, closing is really a dummy operation, and it is not an error to omit it.

getS returns either a line of text or, once it encounters the end of its input source, the atom false. It is actually defined as a one-argument procedure, and as such sends back its result as its argument’s value. However, here we follow the common practice in Oz to call a procedure in a function style by putting a $ in place of the return argument: that argument’s value is returned then as a function value.

The EvalRpns procedure’s job is to take a line from the ‘lazy’ input list, Lines, strip possible whitespace from both its ends, then split the string up into a list of tokens, Tks. If non-empty, Tks is processed and the result – a number or the atom error – gets printed by showInfo. (The atom unit, used as a delimiter specifier in the calls of both strip and split, instructs those functions to use the default delimiter, whitespace.) Finally, EvalRpns calls itself again on the remainder of the input list.

In this procedure, as well as elsewhere in the program, .1 is a selector for the head of a list, and .2 for its tail. This is because lists are really tuples in Oz, more precisely pairs, of the form (head,tail), and this is the only direct way provided for accessing their constituents. Because strings are lists, this also applies to them.

All variables, or defined names in Oz, including the names of procedures, functions, and modules, begin with a capital letter, to distinguish them from the keywords and the symbolic atoms. A name must be declared in the program, even if not immediately bound to a value, before it is used. In almost all cases, the scope of a declaration is specified via a local…in…end construct, which we have encountered already in the main portion of the program. Very often, namely in the context of if, case, proc etc. which have their own scope delimiters, the local and end keywords of a declaration can be omitted. This is the case wherever a sole in is seen in the program.

The very processing of an RPN expression is done within a call to the library function FoldL, similar in name and service to other functional languages: it traverses the list Tks applying the ‘accumulating function’ EvalTk as it does. Nesting the call of FoldL within a try…catch construct makes it possible to emit a certain value, which we have chosen here to be false, should an exception happen to take place in the course of computing the RPN expression. The case…of… checks the eventual result and produces either a number or the error atom as the value to be printed by showInfo. At this point, a correctly evaluated RPN expression results in a single-item list, and that item, R, is the number to print. A list of more than one items, as well as the false atom, do not match the [R] pattern, which is precisely when the processed RPN expression was erroneous.

EvalTk has a stack (St) and a token (Tk) as its arguments. The stack is represented by a list value, initially empty (nil), and must get larger with every token that is a number, and smaller when the token is an operation. In fact, St does not change; a new stack is produced and returned. EvalTk checks which kind of token it sees and proceeds accordingly. If Tk is an arithmetic operator, a case…of… expression deconstructs St so as to extract the values X and Y of the needed arguments. The first and only character in Tk, Tk.1, is converted to an atom – one of +, -, *, and / – in order to obtain in Op the corresponding procedure. The first three of these procedures (Number.'+', Number.'-', and Number.'*') reside within the module Number, while the division procedure is in Float. The library procedure CondSelect is used to make the discrimination: if the given atom is a feature name within Number, the module member of this name is chosen; otherwise Op is bound to Float.'/'. The result of appplying Op to X and Y is appended in front of the remainder T of the stack.

Failing to be an operator, Tk is passed to EvalNum, which tries to parse it as a number. If Tk is indeed one, that number is returned, and eventually gets pushed onto St to produce a new stack. If Tk is not a number, then it is an incorrect token and EvalNum raises an exception. An exception from EvalNum, as well as one raised due to being impossible to obtain X and Y from St when Tk is an operator, pass control directly to the catch within EvalRpns.

For extracting a number from its parameter S, EvalNum basically makes use of StringToFloat – a library function. It is that function that raises an exception if the given token is incorrect. Compared to what we consider a valid number, StringToFloat exhibits some limitations. It does not accept integers, numbers with the decimal dot in a leading position, and numbers with arithmetic sign + or -. Furthermore, it does accept floats with ~, which denotes a negative number in Oz, but ~ is something that our calculator should consider erroneous. Therefore, we have to analyze a bit, and possibly modify the string S before it gets passed to StringToFloat.

First, a leading + or -, if present, is skipped and taken account for by setting the value of Sgn. The thus obtained ‘new’ string U (which is either S or its tail) is checked against the presence of punctuation characters. If none is found, a dot is appended to ensure that the number is a floating-point one, resulting in a still newer string, V. Finally, a 0 is added in front of V, but only when V does not consist of a single character. By doing this we want to ensure that there is no leading dot in the string, but if the string consisted of the dot alone, we leave it as it is, because in this case adding a 0 would have rendered an incorrect string correct. Adding a 0 also makes strings starting with ~ incorrect for StringToFloat. Eventually, the said removals and additions guarantee that StringToFloat produces a correct result for all the tokens that we want it to do so, and raises an exception in all cases that we consider erroneous.

Some of the library functions used in the program are readily accessible. For others, the names of the modules where those functions belong, must be imported explicitly. In addition, for the name String we need to specify the module’s filename, relative to where the Oz system is located; there is another module with the same name which is imported implicitly.

Normally, function names are prefixed by the names of their modules, but for a number of functions from the implicit modules Oz (also implicitly) provides aliases. For example, Length’s complete name is List.length, where length is an exported feature of the module List. In general, aliasing is a convenience available to the programmer: any name can be aliased as needed.

import Application System Open
       String at 'x-oz://system/String.ozf'
  proc {EvalRpns Lines}  EvalTk  in
    fun {EvalTk St Tk}  EvalNum  in
      fun {EvalNum S}  Sgn U V  in
        U = if {Member S.1 "+-"} then
              Sgn = if {Char.toAtom S.1}=='+' then 1. else ~1. end
              Sgn = 1.
        V = if {Some U Char.isPunct} then U else {Append U "."} end
        Sgn*{StringToFloat if {Length V}>1 then &0|V else V end}
      end  % EvalNum
    % begin EvalTk
      if {Length Tk}==1 andthen {Member Tk.1 "+-*/"} then
        Op = {CondSelect Number {Char.toAtom Tk.1} Float.'/'}  in
        case St of Y|X|T then {Op X Y}|T end
        {EvalNum Tk}|St
    end  % EvalTk
  % begin EvalRpns
    if Lines\=nil then
      Tks = {String.split {String.strip Lines.1 unit} unit} in
      if {Length Tks}>0 then
        {System.showInfo case try {FoldL Tks EvalTk nil}
                              catch _ then false end
                         of [R] then R else error end}
      {EvalRpns Lines.2}
  end  % EvalRpns
% begin main
    class TxtFile from Open.file Open.text end
    File = {New TxtFile init(name:stdin flags:[read text])}
    fun lazy {ReadIn}
      case {File getS($)} of false then nil
      []  L then L|{ReadIn} end
  in {EvalRpns {ReadIn}} end
  {Application.exit 0}