Bored with Perl? Looking for a cool language to learn? Then why not try Forth? This post is aimed to tease you as to the possibilities of Forth, and hopefully to get you to try it. As usual, I will use my own version of Forth, which you can find here.
I have an accounts package that I have written in Perl. I often think it’s fun to try to see how I might approach the problem if I wrote it in Forth. Perl processes a file containing a list of commands. Commands are either “#”, for comments, “ntran” for normal transactions, and “etran” for equity transactions.
In most languages, you’d have to write some kind of parser to break commands down into type and arguments, and dispatch on on the command. With Forth, you can use the Forth parser itself to do the parsing for you. You don’t even need to dispatch on the command; the commands can be defined as Forth words themselves, which the parser just executes.
Done well, Forth can therefore feel very much like a DSL. Although the plumbing might feel a bit messy sometimes, when you reach the top level, the code itself can look very “crisp”.
An “ntran” consists of a few fields: a date stamp, a debit account, a credit account, an amount, and a description. A typical example:
ntran 2020-04-18 bank cash 10.00 "banked some cash"
Let’s suppose we want to have variables for each of these fields. The standard way in Forth is to use the word VARIABLE, like so:
variable dstamp \ defines the variable for date-stamp variable dr \ for the debit account
and so on.
But that gets a little tedious. Suppose we want to define multiple variables at once. To that end, it would be convenient if we had a word like VARS:, which scanned the rest of the input up until the newline, and defined new variables for each token on that line. In other words, we wanted to be able to do something like:
vars: dstamp dr cr amount desc
Here’s how we could implement it:
: VARS: begin parse-word dup while $create 0 , repeat drop ;
You may be able to see a begin … while … repeat loop there. It’s a loop that parses the next word in the input to a string (via parse-word), and then does something similar to the variable word (via $create 0 ,). The “drop” at the end clears a dangling parse-word.
We can then define the word “ntran” that reads word from the data, and assigns values to variables. We want to repeat the trick that we used for vars:, to write something like:
: ntran store: dstamp dr cr amount desc process-ntran ;
where PROCESS-NTRAN does some kind of further processing that needn’t concern us here. The word we’re interested in is STORE:, which will have the same outer looping construct over the line as VARS:, but a different inner body.
A pattern is developing, so let’s try to abstract away that pattern. We want to be able to write something like:
: VARS: line( $create 0 , )line ; : STORE: line( swap ! )line ;
where LINE( is effectively the phrase “begin parse-word dup while”, and )LINE is the phrase “repeat drop”. We need to turn those “phrases” into “macros”. Here’s how:
: LINE( postpone begin postpone parse-word postpone dup postpone while ; immediate : )LINE postpone repeat postpone drop ; immediate
In this case, the “macrofication” of the phrases is relatively straightforward. You define “macros” by making them immediate, and stick POSTPONE in front of each word. It’s not always that straight-forward, unfortunately, but in this instance we have avoided some of the complexities that can arise.
There are other possible strategies you could use to achieve the same effect, but I won’t explore them here.
I hope that you found that interesting and piqued your interest as to how Forth can be made to look like a very high level language with terse and flexible semantics, even if you didn’t understand much of it.
Update: I just realised that my code for STORE: won’t work as-is, as it needs to be IMMEDIATE. A correct implementation is:
: STORE: line( postpone parse-word postpone pt find cell + postpone literal postpone ! )line ; immediate
We can see an implementation:
vars: a b c : foo store: a b a @ . b @ . cr ; foo 10 20 \ outputs 10 20 foo 30 40 \ outputs 30 40