Wednesday, January 05, 2022

Surprisingly Elegant: Implementing Modules in Forth

Recently, I've been experimenting with problem solving in Forth. The experience has been a mix of embracing the familiar (keeping definitions small, focusing on clean abstractions, not underestimating the power of refactoring) as well as stepping outside my comfort zone (learning to think in postfix, embracing loops over recursion, wrapping my head around a global parameter stack). One side effect of Forth's simplicity that's nagged at me was its Everything Is Global philosophy.

What I kept wanting was a simple module system that would let me classify words as being either public or private to a given file.

The fact that Forth doesn't come with a module system is far more feature than bug. Forth, like Scheme, is a language that strives to meet two seemingly contradictory goals: (1) the core language should be as compact as possible. (2) Programmers should be able to build sophisticated abstractions with ease. Building a module system, in addition to supporting the problems I was solving, would be an elegant test of meeting these two principles.

After many false starts, I finally arrived at a solution. Two quick warnings:

  1. I'm a Forth newbie, so this code is almost certainly problematic.
  2. This code runs on gforth and may not run on other Forth implementations.

With those warnings out of the way, let's check out what I built.

Modules In Action

Here's a verbose and somewhat contrived example of a module:

\ Forth module for working with different units of temperature

module

:private scale-up ( x -- x-scaled )
    100 * ;

:private scale-down ( x-scaled -- x )
    50 + 100 / ;

private-words

create unit-symbols
char C c,
char F c,
char K c,

: the-sym ( index -- char )
    unit-symbols + c@ ;

: C ( -- char ) 0 the-sym ;
: F ( -- char ) 1 the-sym ;
: K ( -- char ) 2 the-sym ;

: deg. ( value unit-c -- )
    swap . ." deg " emit ;

public-words

: deg-c ( c -- t )
    scale-up ;

: deg-f ( f -- t )
    scale-up 3200 - 5 9 */ ;

: deg-k ( k -- t )
    scale-up 27315 - ;

: as-deg-f ( t -- f )
    9 5 */ 3200 + scale-down ;

: as-deg-c ( t -- c )
    scale-down ;

: as-deg-k ( t -- k )
    27315 + scale-down ;

: deg-c. ( t -- )
    as-deg-c C deg. ;

: deg-f. ( t -- )
    as-deg-f F deg.  ;

: deg-k. ( t -- )
    as-deg-k K deg. ;

publish

A module starts with the word module and is finalized by the word publish. In between, the programmer can use the words public-words and private-words to delineate sections of code that are public and private.

As the above example shows, it's possible to toggle back and forth between public and private words.

One fun bit of syntatic sugar is the defining word :private. This creates a colon definition but does so in the private word space.

And here's an example of how the module is used:

\ temperature example, useful for demonstrating modules & tests

require lib/modules.fs
require lib/utils.fs
require lib/testing.fs
require lib/temps.fs

require tests/utils.fs
require tests/modules.fs
require tests/temps.fs

run-all

: tab ( -- )
    5 0 u+do space loop ;

10 constant chart-incr

: f-chart. ( low high -- )
    chart-incr + swap u+do
        cr i deg-f
        dup deg-f. tab
        dup deg-c. tab
        deg-k.
    chart-incr +loop cr ;

Other than a few require's, modules are invisible. deg-f. is public, so you can execute it, the word F is private so it's not visible.

For completeness, here's the output of the above 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
s" /home/ben/dt/i2x/code/src/master/forth/temps.fs" included 9 Tests Run, 9 Passed, 0 Failed ok
0 100 f-chart.
0 100 f-chart. 
0 deg F     -18 deg C     255 deg K
10 deg F     -12 deg C     261 deg K
20 deg F     -7 deg C     266 deg K
30 deg F     -1 deg C     272 deg K
40 deg F     4 deg C     278 deg K
50 deg F     10 deg C     283 deg K
60 deg F     16 deg C     289 deg K
70 deg F     21 deg C     294 deg K
80 deg F     27 deg C     300 deg K
90 deg F     32 deg C     305 deg K
100 deg F     38 deg C     311 deg K
 ok

Implementing Modules

I'm amazed at how little code I needed to implement my module system. You can find the complete source code here. Here's how the code breaks down:

Creating a module adds two new wordlists to the wordlist stack: one for public words and one for private words. Notably, the private word list is on top of the stack.

: module ( )
    wordlist >order ( public )
    wordlist >order ( private )
    public-words ;

The words public-words and private-words use set-current to set the wordlist that newly compiled words are appended to.

: public-words ( -- )
    get-order >r
    over set-current
    r> set-order ;
    
: private-words ( -- )
    get-order >r
    dup set-current
    r> set-order ;

And finally, publish invokes previous which drops the top wordlist, that is, the private wordlist.

That's it; that's the entire system. The code works because dropping the private wordlist removes the ability to execute its word by name, while references to it are tied to absolute IDs which are left untouched. I do believe I've added the functionality I was after while remaining in the spirit of Forth.

The syntactic sugar :private is perhaps some of the most elegant code I've ever written in any language. Check it out:

: :private private-words : public-words ;

The definition of :private simply switch to private words, defines the word using : and switches back to public words.

Lessons Learned

I'm quite pleased with this little detour to craft a module system. I not only built a useful abstraction that simplifies future problem solving, but I did so in a way that has, to me, highlighted a number of Forth's strengths.

No comments:

Post a Comment