How to Design Programs - in Forth
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.