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.
parserpn procedure is responsible for ensuring that the RPN expression is correctly formed. First, by calling
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
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* ;