Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Recursive Techniques

Final provides support for writing recursive code, and code that uses forward references. A good example is Final’s own json parsing code. The words that parse individual Json types, needs to be able to deal with any Json type. For example, a Json array can contain items of any Json type.

This is re-entrant recursion, and is an appropriate technique when the depth is not expected to be too deep. (What is “too deep”? We’ll get to limits later.)

Other times, an algorithm can be succinctly expressed as a step that subdivides or simplifies the work, then calls itself. Factorial is an often cited example.

Simple Recursion

During compilation of a Final word, the word itself is not yet visible in the dictionary. This presents a challenge in trying to use the word itself within itself. In this situation, Final provides a keyword, recurse; this has the same effect as naming the word itself; at runtime, it will nest into a new instance of the word.

// ( N:i -- N!:i )
: factorial
  dup 1 > if
    dup 1 - recurse *
  else
    drop 1
  then
;

When N > 1, the word keeps a copy of N, computes (N-1)! via recurse, then multiplies. For 0 and 1, the base case drops the input and leaves 1.

Forward References

Json parsing is a good example of re-entrant recursion. In this type of code, a word may not itself be recursive, but will indirectly call code that causes the nesting level of the overall algorithm to increase.

In this type of code, there is typically a chicken/egg in which earlier defined code must be able to call later-defined code. However Final, as it is defined, cannot directly support forward-references–a Word needs to be defined before it can be compiled.

In this case, Final provides defer:. It creates a word with no behavior yet. You assign the behavior later:

&of realWord is deferredWord

&of names the word that does the work. is makes that the behavior of the deferred word. You can compile calls to the deferred name before the real word exists. Until is runs, calling the deferred word panics.

You can also change that behavior later. A program can start with a basic version, then replace it once more of the system is defined.

Here is a short piece of Final’s own Json parser. Arrays and objects both contain values, and a value may itself be an array or object, so jsonParseArray and jsonParseObject call jsonParseValue before that word’s body is written:

// sole cycle: object/array members → value → object/array
defer: jsonParseValue

// ( -- list:List )
: jsonParseArray
  …
  jsonParseValue    // may nest into another array or object
  …
;

// ( -- obj:DynamicObject )
: jsonParseObject
  …
  jsonParseValue
  …
;

// ( -- v:a )
: (jsonParseValue)
  …
  // '"' → string | '{' → object | '[' → array | …
;

&of (jsonParseValue) is jsonParseValue

defer: jsonParseValue creates the name so the array and object parsers can call it. The real work is (jsonParseValue). The last line assigns that word’s behavior to jsonParseValue. Until that line runs, calling jsonParseValue panics.

Limits

Recursive and Re-entrant code can often provide a concise, easy to reason about algorithm. However these techniques are only suitable within limits of the level of recursion that happens at runtime. Final places depth limits on all its stacks, including the Return Stack. These limits are well above what almost any typical code would stress. But a recursive algorithm that results in excessive nesting can hit a data stack or return stack limit. In Final, that is a bit over 1,000.

So recursive/reentrant code needs to itself have limits. Recursion can always be replaced with iteration, so if the data set or recursion depth will exceed Final’s limits, then it should be reworked using an iterated type of approach.