RPN Calculator in Factor

Computing RPN expressions is trivial in Factor: a properly constructed expression is a Factor program, so the corresponding string can be directly evaluated to yield the result. However, we still need first to tell a correct expression from an incorrect one. Almost all of the code in the following program is there precisely to take care of that.

The program starts with adding some vocabularies to the search path maintained by the interpreter as it executes the script, so that the definitions of many words needed for doing I/O, parsing, working with datatypes etc. become available. Only a very small number of words are accessible to the script by default.

The main procedure rpncalc reads text lines from the standard input until there are no more. Each line is passed to evalrpn for processing and then rpncalc calls itself (tail-)recursively.

evalrpn splits a line into tokens, thus forming an array of strings. Depending on the input containing series of consecutive blanks, some of the entries in the resulting array may be empty strings instead of true tokens. These are removed (calling subset) and if the remaining token sequence is not empty, further processing takes place.

The parserpn procedure is responsible for ensuring that the RPN expression is correctly formed. First, by calling map, enc is applied to each token so that the latter is mapped to 1 or -1, for a number and an operator, respectively. enc uses the two procedures nm? and op? as predicates to test if a string matches the corresponding kind of token. Then the mapped numeric sequence is passed to accumulate to get a sequence of cumulative sums of those 1s and -1s. For the RPN expression to be correct, all cumulative sums must be positive, and the last one of them must be 1. accumulate is one of the combinators in Factor that provide an abstract, general form of functional-style iteration over sequences (analogous to, say, scanl in Haskell).

enc throws an exception if a token is neither a number nor an operator. An exception is also thrown in parserpn when a cumulative sum fails the above said test. Thus any syntax error in an RPN expression eventually causes an exception to occur and propagate through outside parserpn where it is caught by catch. Note that for this to happen the call of parserpn is embedded in a quotation that is called by the catch. The outcome of that catch is checked in an if, so that raising an exception yields printing an error message, whereas, if no such occurs, the expression is correct and it gets evaluated by calling eval on the original input string, and finally the result is printed by applying the dot (.) command on it.

The cast to a floating-point type (the call of >float) before printing can be omitted, which will ensure outputting an exact rational number when none of the numbers in the given expression is floating-point.

Note that in nm? we use the library procedure string>number to parse a string token and convert it into a number. The only point in devising nm? at all is to check if the token contains a / character (represented by ASCII 47). Such characters are part of the syntax for rational numbers which are a valid input for string>number but are not acceptable according to our RPN expression specification. By wrapping string>number with this additional check into a new procedure it is ensured that input like 12/4 (which would read as 3) or 5/7 (reading as a true rational) is rejected.

USING: kernel io parser sequences math prettyprint errors ;

: nm? 47 over member? [ drop f ] [ string>number ] if ;

: op? dup length 1 = swap first "+-*/" member? and ;

: enc dup nm? [ drop 1 ] [ op? [ -1 ] [ throw ] if ] if ;

: parserpn [ enc ] map unclip [ + ] accumulate
           [ 0 > ] all? swap 1 = and [ throw ] unless ;

: evalrpn
  dup " \t" split [ "" = not ] subset
  dup empty? [ 2drop ] [
    [ parserpn ] catch
    [ 2drop "error" print ] [ eval >float . ] if
  ] if ;

: rpncalc readln [ evalrpn rpncalc ] when* ;