Wednesday, 4 December 2024

FORTH and the Rosetta Code Challenge

The other day I came across this Rosetta Code challenge and thought that seems simple enough for me to have a go at... using RobsForth! Perhaps, I misunderstood what the challenge was really about? As far as I understood it, most languages don't have (COBOL does) an IF conditional that can be fed two truth tests and will output:

  1. Both tests are True
  2. The first test is True 
  3. The second test is True
  4. Neither test is True

The challenge was to reformulate the typical solution using nested IF's  given below into a function called IF2 which can be used instead of the nested IF's anywhere in your code.

There were lots of languages on display, quite a few of them esoteric one-off's like RobsForth, as well as the usual suspects like C++, Java and Python. I was surprised how complex some of the solutions were. Some submitters provided their function without a test suite, but we all know how easy it is to introduce a logic error, even on the simplest code.

Rosetta Code's introduction assured me that Lisp and Forth automatically support language extension, so I figured this shouldn't be a problem 😆 and away I went. My first effort was too complex, but eventually I pared everything down and ended up with:

# cond1 cond2 -- int
: if2
	IF IF 3 exit THEN 2 exit THEN
	IF 1 exit THEN
	0
;

a Forth word that eschews the ELSE clause.  Forth, of course, is a stack based language and while RobsForth does have global variables, it doesn't have local variables and so everything you see below relies on the stack to store state.

"""
	if2 
	Rosetta Code challenge 
	https://rosettacode.org/wiki/Extend_your_language

if (condition1isTrue) {
     if (condition2isTrue)
        bothConditionsAreTrue();
     else
        firstConditionIsTrue();
  }
  else if (condition2isTrue)
     secondConditionIsTrue();
  else
     noConditionIsTrue();

if2 (condition1isTrue) (condition2isTrue)
     bothConditionsAreTrue();
  else1
     firstConditionIsTrue();
  else2
     secondConditionIsTrue();
  else
     noConditionIsTrue();
"""
  • Anything between """ and """ is a multi-line comment
  • # is a single line comment
# ----------------- if2 Definition ------------------------

# cond1 cond2 -- int
: if2
	IF IF 3 exit THEN 2 exit THEN
	IF 1 exit THEN
	0
;

# ----------------- if2 Test ------------------------------

# int -- stdout
: branch
	dup
	3 = IF drop |+ ' is divisible by 2 & 3 ' +| exit THEN
	dup
	2 = IF drop |+ ' is divisible by 3 ' +| exit THEN
	dup
	1 = IF drop |+ ' is divisible by 2 ' +| exit THEN
	drop
	|+ ' is not divisible by 2 or 3 ' +|
;

The above word, called branch, demonstrates much of what RobsForth is about:

  • IF consumes a truth value, where 0 is false and anything else is true.  The truth value is supplied by the stack.
  • Words between |+ & +| are prepended to the words queue and executed immediately, before returning to continue executing the compiled word.
  • Words between ' & ' are printed to std out.  Inside a compiled word they must be surrounded by |+ & +| otherwise ' & '  would consume the words queue AFTER this compiled word.
  • exit jumps out of a compiled word.  It turns a sequence of IF statements into a Case statement, rather than the deep nesting of IF ELSE's that would otherwise be required e.g.
    IF ELSE
    IF ELSE
    IF ELSE
    THEN
    THEN
    THEN
# --
14 0 BEGIN
   dup dup dup dup dup . space . 
   3 / swap 3 / int - 0 = pob
   2 / swap 2 / int - 0 = pob
   top top if2 branch /n
next WHILE drop drop

RobsForth doesn't have to compile words before using them. Code can just be a script as above. The only native conditional words in RobsForth are:

  • IF ELSE THEN where IF is really Jump==0 while ELSE is Jump and THEN is a jump marker for IF or ELSE.
  • BEGIN WHILE where WHILE is really Jump!=0 and BEGIN is its jump marker.

RobsForth uses a deque rather than a stack and pob and top are moving items from the top of the deque to the bottom and vice versa. Consequently, RobsForth has four easily accessible local variable slots rather than a typical Forth's two.  RobsForth does not implement complicated stack manipulators like pick, roll, rot, nip, tuck, etc, etc... just:

  • dup, swap, over, drop at the top of the deque.
  • dob, sop, oob, bop, pob, top, beep on the bottom of the deque. beep is equivalent to peek, but there is no peek in Forth, because Forth words automatically consume the stack when they are invoked and may leave items on the stack when they exit. This implied use of the stack for local variables removes the need to explicitly set and get local variables. The stack is an interesting structure:
    • Variables on it are transient globals whose lifespan is determined by their depth on the stack. In most languages local variables must be explicitly passed by value to another function. In Forth, variable creation, passing and destruction is implied.
    • The state of the program can be inferred by looking at the stack because it contains all local variables. RobsForth also stores true global variables on it's heap which live for the duration of the script, but whenever possible, I try to avoid using heap variables.
    • Because RobsForth has no local variables, refactoring is straight forward.  Simply extract a sequence of words from a word and call them a new word. The key to refactoring is to have word names that accurately describe what that word is doing. If this is done then RobsForth becomes a domain specific language and understanding code means comprehending the major words in the task's context.
# ----------------- if2 Results ----------------------------

<!-- Forth --> @test.fr
0 is divisible by 2 & 3
1 is not divisible by 2 or 3
2 is divisible by 2
3 is divisible by 3
4 is divisible by 2
5 is not divisible by 2 or 3
6 is divisible by 2 & 3
7 is not divisible by 2 or 3
8 is divisible by 2
9 is divisible by 3
10 is divisible by 2
11 is not divisible by 2 or 3
12 is divisible by 2 & 3
13 is not divisible by 2 or 3
<!-- Forth --> 

I hope this gives you an idea of what RobsForth looks like. It's just a stream of white space separated tokens, which I have formatted, so that they are easy to look at.

A key objective with RobsForth is to keep software development simple. Below are three acronyms the industry uses in an attempt to keep software simple.

  • KISS: Keep it simple stupid (”Write the code in the simplest way you can think of. Refactor constantly.”)
  • YAGNI: You ain’t gonna need it (”Don’t write the code if you don’t need it now”)
  • DRY: Don’t repeat yourself (”Do not copy-paste”)

Chuck Moore, the inventor of Forth, was a firm believer in KISS and YAGNI (DRY was probably so obvious to him, that he never bothered to mention it!) and I think the following acronym too.

  • MS: Minimise syntax (”Avoid languages with excessive syntax”)
    • Since using RobsForth, I no longer like languages which expect syntax. They're too busy which makes it harder to concentrate on the problem at hand.  White space syntax languages are the worst, which is ironic, since RobsForth is implemented using Python!