How to Design Programs is a classic text, illustrating how to implement small projects, specifically breaking them down into small functions.

It’s originally written for Scheme which is definitely a high-level language.

When I first started writing software for a living, I wrote a fair bit of code in C and 8086 Assembly, specifically I wrote code for interfacing with serial terminals, this is before the Internet, and before TCP/IP become ubiquitous and so “green screens” were common in settings, which were TTYs connected to Unix systems via terminal interfacing hardware.

At this time, C was the dominant language for this kind of work, with some bits dropping into assembly where the C wasn’t quite fast enough, which on base level 80286 and 802386 hardware of the era, wasn’t that uncommon.

Around this time, I learned of a language called Forth, and I remember experimenting with Pygmy for DOS mostly out of curiosity, it was easy enough to talk to the 8250/16550 serial devices that were prevalent pre-USB.

Forth is an interesting language, it’s designed to be very low-level, for interfacing with hardware, and so, like C it’s easy to drop into assembly.

The syntax is definitely unlike anything I’m familiar with, everything is done with RPN and there are multiple stacks in use to use for passing parameters around, this use of the stack leads to a lot of stack manipulation, and designing small functions involves designing the layout of the variables on the stack, and how they’re moved around in the code, this leads to small functions as it becomes increasingly hard to reason about the state of the stack in code as functions get larger.

I’m not going to go into Forth syntax too heavily here, the best books on the subject are freely available:

  • Starting Forth by Leo Brodie https://www.forth.com/starting-forth/
  • Thinking Forth by Leo Brodie https://thinking-forth.sourceforge.net/

Looking at the examples in the HtDP book…

(define (area-of-disk r) 
  (* 3.14 (* r r)))

This declares a function area-of-disk that takes a parameter r it then multiplies r by itself, and then by 3.14 which is an approximation of Pi.

The simple approach to this looks like this:

: area-of-disk ( n -- n )
  dup * 314 * 100 / ;

Trying this in Gforth

Gforth 0.7.3, Copyright (C) 1995-2008 Free Software Foundation, Inc.
Gforth comes with ABSOLUTELY NO WARRANTY; for details type `license'
Type `bye' to exit
: area-of-disk ( n1 -- n2 )  compiled
  dup * 314 * 100 / ;  ok
5 area-of-disk . 78  ok

In the example, I’m pushing 5 onto the stack, then calling area-of-disk which executes and pushes the result onto the stack, running . pops the top element off the stack, with the result 78.

Looking at each operation, and how it affects the stack:

operation stack
5 5
area-of-disk 5
dup 5 5
* 25
314 25 314
* 7850
100 7850 100
/ 78

In the book, the result should be 78.5, but this code uses purely integer arithmetic which isn’t ideal.

Reimplementing this in Forth to use the floating-point stack:

: farea-of-disk ( r1 -- r2 )
  fdup f* pi f* ;

Trying this in Gforth looks slightly different:

Gforth 0.7.3, Copyright (C) 1995-2008 Free Software Foundation, Inc.
Gforth comes with ABSOLUTELY NO WARRANTY; for details type `license'
Type `bye' to exit
: farea-of-disk ( r1 -- r2 )  compiled
  fdup f* pi f* ;  ok
  ok
5e farea-of-disk f. 78.5398163397448  ok

The stack operations look almost identical:

operation stack
5e 5.0
farea-of-disk 5.0
fdup 5.0 5.0
f* 25.0
pi 25 pi
f* 78.53981633974

This definitely looks like a more acceptable result.

There are two user stacks in Gforth, the stack and the floating-point stack, these are maintained separately, and the e notation pushes the number 5.0 onto the floating-point stack.

There is a mirrored set of functionality for operating on the floating-point stack.

pi is defined as a floating-point constant with 3.14159265358979.

The next part involves calculating the area of a ring, which is one circle inside of another:

(define (area-of-ring outer inner) 
  (- (area-of-disk outer)
     (area-of-disk inner)))

Reimplementing this in Forth is fairly simple and short:

: farea-of-ring ( r1 r2 -- r3 )
 farea-of-disk fswap farea-of-disk fswap f- ;

This time there’s a bit of stack rearrangement to get this to work:

operation stack
5e 5.0
3e 5.0 3.0
farea-of-ring 5.0 3.0
farea-of-disk 5.0 28.2743338823081
fswap 28.2743338823081 5.0
farea-of-disk 28.2743338823081 78.5398163397448
fswap 78.5 28.26
f- 50.24

Running the code:

Gforth 0.7.3, Copyright (C) 1995-2008 Free Software Foundation, Inc.
Gforth comes with ABSOLUTELY NO WARRANTY; for details type `license'
Type `bye' to exit
: farea-of-disk ( r1 -- r2 )  compiled
  fdup f* pi f* ;  ok
: farea-of-ring ( r1 r2 -- r3 )  compiled
 farea-of-disk fswap farea-of-disk fswap f- ;  ok
5e 3e farea-of-ring f. 50.2654824574367  ok

There are two fswaps in this code, fswap swaps the top two elements on the floating-point stack.`

Without this, the second area-of-disk would use the top element on the stack, which would be the result of area-of-disk on the first item.

The second fswap orders the areas of the two disks for the f- call.

This could be avoided by changing the ordering of the parameters on the stack, but this would put the onus on the caller to remember that the inner circle radius comes first which is not ideal.