Output & Running It
Hello, World
Haskell needs a
main of type IO () for the runtime to find. Forth has no entry point at all: the file is read top to bottom and each line runs as it is read, so defining HELLO and calling it are two separate acts.main :: IO ()
main = putStrLn "Hello, World!": HELLO ." Hello, World!" CR ;
HELLOThere is no distinction here between a script and a program, and no
IO to be inside — every word can print, and nothing in a type says which ones do. That is the first thing a Haskell reader gives up, and the rest of the page follows from it.Showing A Number
print is putStrLn . show, so it needs a Show instance to know how to render its argument. Forth's . prints the cell on top as a signed number, because a cell is the only thing there is to print.main :: IO ()
main = print (6 * 7)6 7 * . CRThere is no
show and no dispatch: . prints signed, U. prints the same bits unsigned, and choosing the word is choosing the rendering. A value carries nothing that could be consulted.Compiling While Running
EVALUATE takes an address and a length and interprets the text as Forth source immediately, with the same interpreter that read the rest of the file. The compiler is an ordinary word and is present at run time.main :: IO ()
main = do
-- GHC has no eval: this string is data, and stays data.
let source = "2 + 3"
putStrLn ("cannot run: " ++ source): RUN-SOURCE S" 2 3 + . CR" EVALUATE ;
RUN-SOURCEHaskell separates compilation from execution completely, and the type checker is the reason — there is nothing to check a string against until it is parsed. Forth has nothing to check, so the boundary never had to exist.
Composition Is Concatenation
Point-Free Is The Only Style
A Haskell programmer writes point-free when it reads well and names arguments when it does not. In Forth there is nowhere to put a name, so every definition is point-free —
DUP * takes what is on top, copies it, and multiplies.square :: Int -> Int
square = \value -> value * value
main :: IO ()
main = print (square 9): SQUARE ( n -- n*n ) DUP * ;
9 SQUARE . CRThis is the framing that makes Forth click for a Haskell reader: a word is a function from stack to stack, and writing two words next to each other is composing them. Joy, the language that made this explicit, called it exactly that.
There Is No . Operator
Haskell needs
. to compose, because f g means applying f to g. In Forth juxtaposition already is composition, so DOUBLE DECREMENT is the composed word and no operator is needed.main :: IO ()
main = print ((subtract 1 . (* 2)) 10): DOUBLE ( n -- n ) 2 * ;
: DECREMENT ( n -- n ) 1 - ;
: DOUBLE-THEN-DECREMENT ( n -- n ) DOUBLE DECREMENT ;
10 DOUBLE-THEN-DECREMENT . CR🚨 Note the order flips: Haskell's
f . g applies g first and reads right to left, while DOUBLE DECREMENT runs left to right in the order written. Forth is the one that matches the arrow in the type, which is not what a Haskell reader expects.A Pipeline Of Small Words
Haskell composes three whole-list operations, each of which could stand alone. Forth has no list to pass between stages, so the filter, the map and the sum collapse into one loop over the numbers.
main :: IO ()
main = print (sum (map (* 2) (filter even [1 .. 10]))): DOUBLE-EVENS-SUM ( limit -- sum )
0 SWAP 1 + 1 ?DO
I 2 MOD 0= IF I 2 * + THEN
LOOP ;
10 DOUBLE-EVENS-SUM . CRThis is the real cost of having no data structure to thread through: the three ideas stop being separable, and a reader has to disentangle them by eye. It is also why the loop allocates nothing and touches no memory, which is the trade Forth is making.
There Is No Partial Application
Every Haskell function of two arguments is a function of one returning a function, so
add 10 is a value. A Forth word consumes what it consumes when it runs; there is no partially applied word, because there is no application to be partial about.add :: Int -> Int -> Int
add a b = a + b
main :: IO ()
main = do
let addTen = add 10
print (addTen 5): ADD ( a b -- sum ) + ;
: ADD-TEN ( n -- sum ) 10 SWAP ADD ;
5 ADD-TEN . CRSpecializing a word means writing another word that supplies the argument, as
ADD-TEN does. The SWAP is there because 10 goes on top of the incoming value and ADD wants it underneath — the kind of bookkeeping currying does silently.Words Instead Of Functions
The Type Signature Is A Comment
Haskell checks the signature against the body and against every call. The Forth stack comment between parentheses is ignored entirely —
( is a word that skips text up to the next ).midpoint :: Int -> Int -> Int
midpoint low high = (low + high) `div` 2
main :: IO ()
main = print (midpoint 10 20): MIDPOINT ( low high -- middle ) + 2 / ;
10 20 MIDPOINT . CR
: WRONG ( low -- middle ) MIDPOINT ;
.( WRONG compiles fine and will misbehave ) CRIt is still the most important thing in a definition, because it is the only statement of what the word expects and leaves.
WRONG above consumes a cell it was never given and compiles without complaint; the symptom arrives somewhere else entirely.Returning Two Values
Haskell packages two results in a tuple, which is a real value with a type. A Forth word simply leaves two cells where it found its arguments, so several results cost nothing and need nothing to describe them.
divide :: Int -> Int -> (Int, Int)
divide numerator denominator =
(numerator `quot` denominator, numerator `rem` denominator)
main :: IO ()
main = do
let (quotient, remainder) = divide 17 5
putStrLn (unwords (map show [quotient, remainder])): DIVIDE ( numerator denominator -- remainder quotient ) /MOD ;
17 5 DIVIDE
. . CRThe saving is real and so is the loss: a tuple can be passed on, stored, and pattern-matched, while two cells on the stack are only ever "the two things on top". Note the comment says
-- remainder quotient, because /MOD leaves the quotient on top.Recursion Needs A Keyword
A Haskell binding is in scope throughout its own body, so
factorial can call itself by name. A Forth definition is not added to the dictionary until ; runs, so its own name does not yet mean anything.factorial :: Int -> Int
factorial n = if n > 1 then n * factorial (n - 1) else 1
main :: IO ()
main = print (factorial 5): FACTORIAL ( n -- n! )
DUP 1 > IF DUP 1 - RECURSE * ELSE DROP 1 THEN ;
5 FACTORIAL . CRRECURSE refers to the definition being compiled. Writing FACTORIAL inside its own body would find an earlier word of that name — which is how a redefinition can be written in terms of the version it replaces, and has no Haskell equivalent.Purity Versus One Global Stack
Everything Is An Effect
Haskell's type says
double cannot print, read a file, or change anything — it is a function from Int to Int and the compiler enforces it. DOUBLE has the same shape by convention and nothing else.double :: Int -> Int
double value = value * 2
main :: IO ()
main = do
print (double 21)
print (double 21): DOUBLE ( n -- n ) 2 * ;
21 DOUBLE . CR
21 DOUBLE . CRNothing stops a Forth word from printing, writing to memory, or leaving a different number of cells each time it runs. The stack comment is the only statement of purity, and it is a comment — so the discipline Haskell puts in the type system lives here in the programmer's head.
Mutable State Needs No Ceremony
Haskell has mutable references, but reaching one means being in
IO and saying so in every type along the way. VARIABLE makes a word that pushes an address, and @ and ! read and write it from anywhere.import Data.IORef
main :: IO ()
main = do
counter <- newIORef (0 :: Int)
modifyIORef counter (+ 1)
modifyIORef counter (+ 1)
readIORef counter >>= printVARIABLE COUNTER
0 COUNTER !
: BUMP ( -- ) COUNTER @ 1 + COUNTER ! ;
BUMP BUMP
COUNTER @ . CRThe Haskell version is longer because the effect is tracked; the Forth version is shorter because it is not. Neither is careless — they are two answers to the same question, and one of them gives you a compiler that knows which functions can surprise you.
One Stack, Shared By Everything
Haskell's
leftover is a binding in a scope, untouched by the addition beside it. The 99 here is on the same stack every word uses, sitting underneath while 1 2 + runs on top of it.main :: IO ()
main = do
let leftover = 99
print (1 + 2)
print leftover99
1 2 + . CR
. CRThat works only because every word involved is balanced. A word that leaves one cell too many buries the 99 deeper, and one that consumes too many eats it — and neither is reported. This is the failure mode with no Haskell counterpart at all.
No Types At All
One Cell Means Every Type
Haskell distinguishes an
Int from a Char and will not let you confuse them. In Forth 65 is a cell, and whether it is a number or a character is decided by which word you hand it to — . or EMIT.main :: IO ()
main = do
let count = 65 :: Int
let letter = 'A'
putStrLn (unwords [show count, [letter]])65 DUP
. EMIT CRThe same bits are printed twice here, two different ways, from one
DUP. That is the whole type system: the value carries nothing, so the word decides, and handing an address to EMIT is a perfectly valid program.No Type Classes, No Dispatch
A type class picks the implementation from the argument's type, resolved at compile time. There is no type to resolve on in Forth, so the two operations get two names and the caller chooses between them.
class Describe a where
describe :: a -> String
data Sensor = Sensor
data Relay = Relay
instance Describe Sensor where
describe _ = "sensor"
instance Describe Relay where
describe _ = "relay"
main :: IO ()
main = putStrLn (unwords [describe Sensor, describe Relay]): SENSOR-DESCRIBE ." sensor" ;
: RELAY-DESCRIBE ." relay" ;
SENSOR-DESCRIBE SPACE RELAY-DESCRIBE CRThe prefix convention does the grouping a class would do, and cannot do the part that matters: no word can accept "anything describable". Where that genuinely matters, Forth reaches for
DEFER, which is one entry rather than a dispatch table.DEFER Is A One-Entry Dictionary
DEFER creates a word whose behavior is chosen later. ' pushes a word's execution token — its address — and IS installs it, so every caller compiled against EMIT follows the new one.main :: IO ()
main = do
let emit1 = putStrLn "serial"
emit1
let emit2 = putStrLn "silent"
emit2: SERIAL ." serial" CR ;
: SILENT ." silent" CR ;
DEFER EMIT
' SERIAL IS EMIT
EMIT
' SILENT IS EMIT
EMITThis is late binding, opt-in, one word at a time, and it is how a Forth driver is retargeted without recompiling its callers. Nothing checks that the installed word has the right stack effect, which is exactly what a type class signature would have guaranteed.
Numbers Have An Edge
Integer Versus A Fixed Cell
Haskell's
Integer grows without limit, and its Int wraps like a machine word. Forth has only the second kind, at whatever width the system uses — 32 bits here.main :: IO ()
main = do
print (product [1 .. 20 :: Integer])
print ((maxBound :: Int) + 1): FACTORIAL ( n -- n! )
1 SWAP 1 + 1 ?DO I * LOOP ;
20 FACTORIAL . CR
2147483647 1 + . CRTwenty factorial does not fit in 32 bits, so the Forth column prints a wrapped number rather than the real answer, and nothing says so. Reaching for arbitrary precision is not an option here; a program that needs it implements it.
div And quot, Both Present
Haskell names both roundings:
quot truncates toward zero and div floors. Forth names the arithmetic instead — SM/REM is symmetric, FM/MOD is floored, and /MOD is whichever the system happens to use.main :: IO ()
main = do
putStrLn (unwords (map show [(-7) `quot` 2, (-7) `rem` 2]))
putStrLn (unwords (map show [(-7) `div` 2, (-7) `mod` 2]))-7 2 /MOD . . CR
: FLOORED ( n d -- remainder quotient ) >R S>D R> FM/MOD ;
-7 2 FLOORED . . CR🚨
/MOD's rounding is implementation-defined: this Forth truncates and prints -3 -1, while gforth floors and prints -4 1 for the same line. Code that cares must say SM/REM or FM/MOD, and take the double-cell dividend that S>D supplies.No Fractional Type
Haskell has
Double, Rational and a Fractional class; this row avoids all of them so the two columns compute the same thing. This Forth has no fractional anything, so scaled integers are the only option.main :: IO ()
main = putStrLn (show (3300 `div` 1000) ++ "." ++ show (3300 `mod` 1000)): .VOLTS ( millivolts -- )
S>D <# # # # [CHAR] . HOLD #S #> TYPE ;
3300 .VOLTS CRThe
<# … #> sequence is pictured numeric output, which builds the text right to left one digit per #. It exists because . prints a trailing space and so cannot be used to build a number with anything after it.Lists Versus Addresses
There Is No List
CREATE makes a word that pushes its own address and , lays one cell down after it. There is no cons cell, no length, and no type — the array is an address, and the count is a separate constant you keep correct.main :: IO ()
main = putStrLn (unwords (map show [10, 20, 30 :: Int]))CREATE READINGS 10 , 20 , 30 ,
3 CONSTANT #READINGS
: SHOW-ALL #READINGS 0 DO READINGS I CELLS + @ . LOOP CR ;
SHOW-ALLA Haskell list is a value: it can be returned, stored in another structure, and consumed lazily. This is a region of memory whose meaning lives entirely in the words written around it, which is why
#READINGS sits right beside it.foldr Versus A Loop Over Addresses
foldr takes the operation as an argument, so summing and multiplying differ by one character. The Forth loop has the + written into it, and changing the operation means editing the word.main :: IO ()
main = print (foldr (+) 0 [10, 20, 30 :: Int])CREATE READINGS 10 , 20 , 30 ,
: SUM ( count -- total )
0 SWAP 0 ?DO READINGS I CELLS + @ + LOOP ;
3 SUM . CRPassing the operation is possible — store an execution token and
EXECUTE it — but nothing in the language encourages it, and the result is slower and harder to read than writing the loop twice. This is the clearest place where Haskell's abstraction has no cheap counterpart.Passing The Operation Anyway
' pushes a word's execution token and EXECUTE runs whatever token is on top. That is the whole higher-order mechanism, and it works — the previous row's point is that it is unidiomatic, not that it is missing.applyTo :: Int -> (Int -> Int) -> Int
applyTo value operation = operation value
main :: IO ()
main = putStrLn (unwords (map show [applyTo 5 (* 2), applyTo 5 (* 3)])): DOUBLE ( n -- n ) 2 * ;
: TRIPLE ( n -- n ) 3 * ;
: APPLY-TO ( n xt -- n ) EXECUTE ;
5 ' DOUBLE APPLY-TO .
5 ' TRIPLE APPLY-TO . CRA token is an ordinary cell, so it can be stored in an array or reached by arithmetic; it can also be a number that was never a token, which does something undefined. Haskell's
(Int -> Int) is checked, and (* 2) needs no separate definition.A String Is Not A List Of Char
A Haskell
String is a lazy list of Char, which is why length walks it. S" leaves an address and a length as two separate cells, so the length is already known and costs nothing.main :: IO ()
main = do
let text = "borrowed slice"
print (length text)
putStrLn text: TEXT ( -- addr len ) S" borrowed slice" ;
: SHOW TEXT NIP . CR TEXT TYPE CR ;
SHOWNothing binds the two cells together, so
NIP can discard the address and keep the length. Taking a substring is adding to one and subtracting from the other, which is the compensation for having no string library at all.Laziness Versus Reading Left To Right
Everything Happens Immediately
Haskell binds
expensive to a thunk and forces it only because min needs it. EXPENSIVE runs the moment the word is reached, because the interpreter is reading words and running them.main :: IO ()
main = do
let expensive = product [1 .. 10 :: Int]
print (min 42 expensive): EXPENSIVE ( -- n ) 1 11 1 ?DO I * LOOP ;
42 EXPENSIVE MIN . CRThere are no thunks and nothing to force, so a Forth word that computes something the caller turns out not to need has simply wasted the time. The upside is that performance is exactly what the code says it is, with no space leaks to reason about.
No Infinite Structures
Haskell can write the whole infinite sequence and take five, because nothing is produced until it is demanded. Forth has to know the count before it starts, so the loop bound is written into the word.
main :: IO ()
main = putStrLn (unwords (map show (take 5 [0, 10 ..]))): FIRST-FIVE ( -- )
5 0 DO I 10 * . LOOP CR ;
FIRST-FIVEProducing values on demand means either a loop that already knows when to stop, or a word you call repeatedly that keeps its place in a
VARIABLE. There are no coroutines and no generators to build the second one cleanly.Both Branches Are Compiled
Haskell's
&& is an ordinary function that is lazy in its second argument, so the division never happens. Forth's AND is bitwise and takes two values that have already been computed, so avoiding the division means IF.main :: IO ()
main = do
let value = 0 :: Int
print (value /= 0 && (100 `div` value) > 1): SAFE? ( n -- flag )
DUP 0= IF DROP FALSE ELSE 100 SWAP / 1 > THEN ;
0 SAFE? . CRThere is no short-circuiting operator, and writing
DUP 0<> SWAP 100 SWAP / 1 > AND would divide by zero before the AND ever ran. IF … ELSE … THEN is the control structure that does what laziness was doing.Data Types Versus Offsets
A Record Is An Offset Table
A Haskell record declares fields and generates accessor functions with types. In Forth the layout is the definition:
>X and >Y are words that adjust an address, and the record is the convention that those two cells belong together.data Point = Point { pointX :: Int, pointY :: Int }
main :: IO ()
main = do
let point = Point { pointX = 3, pointY = 4 }
putStrLn (unwords (map show [pointX point, pointY point]))CREATE POINT 2 CELLS ALLOT
: >X ( addr -- addr ) ;
: >Y ( addr -- addr ) CELL+ ;
3 POINT >X ! 4 POINT >Y !
POINT >X @ . POINT >Y @ . CRNothing records that the two words address one object, and nothing stops a word from writing something else into
>Y. What Haskell calls a field access is here an addition that has been given a name.CREATE … DOES> Generates Words
COLOR is a defining word: running it creates a new word. Everything before DOES> runs once at definition time and lays the bytes down; everything after it is the code the new word runs when used.data Color = Color Int Int Int
channels :: Color -> [Int]
channels (Color red green blue) = [red, green, blue]
main :: IO ()
main = putStrLn (unwords (map show (channels (Color 128 64 200)))): COLOR ( blue green red -- ) CREATE C, C, C, DOES> ( -- addr ) ;
200 64 128 COLOR PLUM
PLUM C@ . PLUM 1 + C@ . PLUM 2 + C@ . CRThis is a constructor and a metaprogram at once, written with the same words as everything else — there is no separate language for it as there is with Template Haskell. What it cannot do is let the compiler check that
PLUM is used as a color and not as a number.A Sum Type Is A Tag You Check
Haskell's constructors carry the tag and the payload together, and the compiler checks that every case is handled. Here the tag is a number the caller passes alongside the value, and
AREA tests it.data Shape = Circle Int | Square Int
area :: Shape -> Int
area (Circle radius) = 3 * radius * radius
area (Square side) = side * side
main :: IO ()
main = putStrLn (unwords (map show [area (Circle 2), area (Square 3)]))0 CONSTANT CIRCLE
1 CONSTANT SQUARE
: AREA ( size tag -- area )
CIRCLE = IF DUP * 3 * ELSE DUP * THEN ;
2 CIRCLE AREA .
3 SQUARE AREA . CR🚨 There is no exhaustiveness check and no warning: a third shape added later leaves
AREA silently treating it as a square. This is where a Haskell reader feels the loss most sharply, and there is no Forth idiom that recovers it.Control Flow
Guards Versus IF … THEN
The condition is computed before
IF runs, because everything is postfix — 25 > leaves a flag and IF consumes it. THEN marks where the branches rejoin; it does not begin the true branch.report :: Int -> String
report temperature
| temperature > 25 = "warm"
| otherwise = "cool"
main :: IO ()
main = putStrLn (report 30): REPORT ( temperature -- )
25 > IF ." warm" ELSE ." cool" THEN CR ;
30 REPORTReading
THEN as "and then carry on" makes the shape work. There is no otherwise to forget, but also no warning when a chain of conditions leaves a case unhandled — IF without ELSE simply does nothing.A Counted Loop Instead Of Recursion
Haskell iterates by recursion or by a list comprehension, and a counted loop is unusual enough to look out of place. Forth's
DO … LOOP is the ordinary way, and I pushes the current index.main :: IO ()
main = putStrLn (unwords (map show [0 .. 4 :: Int])): COUNT-TO ( limit -- )
0 DO I . LOOP CR ;
5 COUNT-TODO takes the limit and then the start, so 5 0 DO is the half-open range [0, 5) — the limit is pushed first, which is the reverse of how the range reads aloud and a reliable source of off-by-one bugs.An Empty Range Is Not Empty
Haskell's
[0 .. -1] is the empty list, as expected. Forth's plain DO does not test first: 0 0 DO runs the body once and then wraps all the way round the cell.main :: IO ()
main = do
putStrLn (unwords (map show [0 .. (-1) :: Int]))
putStrLn "done": SAFE-COUNT ( limit -- )
0 ?DO I . LOOP ." done" CR ;
0 SAFE-COUNT?DO is the version that tests before the first iteration, and it is what you want whenever the count comes from a variable. This is the single most dangerous default in the language for a reader arriving from lists.Accumulating Without An Accumulator
The Haskell version threads an accumulator through a tail call, which GHC turns into a loop. The Forth version puts the accumulator on the stack and lets the loop add into it, which is the same computation with the recursion already unrolled.
sumTo :: Int -> Int -> Int
sumTo accumulator 0 = accumulator
sumTo accumulator n = sumTo (accumulator + n) (n - 1)
main :: IO ()
main = print (sumTo 0 10): SUM-TO ( n -- total )
0 SWAP 1 + 1 ?DO I + LOOP ;
10 SUM-TO . CRForth has
RECURSE and no tail-call optimization, so deep recursion overflows the return stack — the counted loop is not a stylistic preference but the only safe shape for anything large. The accumulator living on the stack is why no name is needed for it.Maybe, Either And A Flag
Maybe Versus A Value And A Flag
The Forth convention is to leave the value and a flag, with the flag on top, so the caller tests first and only then finds a value underneath. The stack comment
( -- value true | false ) is how that shape is written down.findEven :: [Int] -> Maybe Int
findEven values = case filter even values of
(first : _) -> Just first
[] -> Nothing
main :: IO ()
main = case findEven [3, 7, 8, 9] of
Just found -> putStrLn ("found " ++ show found)
Nothing -> putStrLn "none"CREATE VALUES 3 , 7 , 8 , 9 ,
: FIND-EVEN ( -- value true | false )
4 0 DO
VALUES I CELLS + @
DUP 2 MOD 0= IF TRUE UNLOOP EXIT THEN
DROP
LOOP FALSE ;
: REPORT FIND-EVEN IF ." found " . ELSE ." none" THEN CR ;
REPORTThis is
Maybe without the type: the two outcomes leave different numbers of cells, so a caller that skips the test is unbalanced for the rest of the program rather than merely wrong. UNLOOP before EXIT is mandatory — the loop's index and limit live on the return stack.Either Carries A Reason
Either carries the reason for the failure and case forces the caller to handle both arms. A Forth flag carries one bit, so the reason has to live somewhere else — here, in a caller that already knows what went wrong.risky :: Int -> Either String Int
risky value
| value < 0 = Left "negative"
| otherwise = Right (value * 2)
main :: IO ()
main = case risky (-1) of
Right doubled -> print doubled
Left message -> putStrLn ("recovered: " ++ message): RISKY ( n -- n*2 true | false )
DUP 0< IF DROP FALSE ELSE 2 * TRUE THEN ;
: MAIN -1 RISKY IF . ELSE ." recovered: negative" THEN CR ;
MAINBoth columns print the same words and mean quite different things by them. Where the reason genuinely matters, the Forth idiom is to return an error number alongside the flag and have the caller map it — which is
Either Int a with the mapping left to you.There Is Nothing To Catch
Haskell can catch an arithmetic exception and carry on. This Forth has neither
CATCH nor THROW, so the only way to survive a bad divisor is to test for it before dividing.import Control.Exception
main :: IO ()
main = do
result <- try (evaluate (1 `div` (0 :: Int))) :: IO (Either ArithException Int)
case result of
Right value -> print value
Left _ -> putStrLn "caught": SAFE-DIVIDE ( a b -- result true | false )
DUP 0= IF 2DROP FALSE ELSE / TRUE THEN ;
: MAIN 1 0 SAFE-DIVIDE IF . ELSE ." caught" THEN CR ;
MAINStandard Forth does define
CATCH and THROW; this implementation omits them, and ABORT" is all that is left — which stops the program outright with no way for a caller to intervene. Testing first is therefore not merely the idiom but the only option.Macros Versus IMMEDIATE
Computing At Compile Time
GHC will very likely fold this at compile time, but nothing in the source says so and nothing guarantees it. In Forth the line below the definition runs as the file is read, so
WINDOW demonstrably holds a number computed before the program proper started.samplesPerWindow :: Int -> Int -> Int
samplesPerWindow rate milliseconds = rate * milliseconds `div` 1000
window :: Int
window = samplesPerWindow 48000 20
main :: IO ()
main = print window: SAMPLES-PER-WINDOW ( rate ms -- samples ) * 1000 / ;
48000 20 SAMPLES-PER-WINDOW CONSTANT WINDOW
WINDOW . CRThere is no separate compile-time language with its own rules about what is allowed, because the same interpreter is running either way. That is the thing Template Haskell exists to provide and that
CONSTANT gets for free.An IMMEDIATE Word Is A Macro
An
IMMEDIATE word runs while the word containing it is being compiled, so it can emit whatever it likes into the definition. POSTPONE LITERAL is how it plants a value there, since a bare number is not a word that could be postponed.main :: IO ()
main = do
-- Template Haskell would need a separate module and an extension.
let halfTheAnswer = 21
print (halfTheAnswer * 2): HALF-THE-ANSWER 21 POSTPONE LITERAL ; IMMEDIATE
: ANSWER HALF-THE-ANSWER 2 * ;
ANSWER . CRThis is a macro made of the same material as everything else: a word, on the same stack, able to read ahead in the source. Template Haskell needs a language extension, a separate compilation stage, and a quotation syntax to do the equivalent.
Adding A Control Structure
Haskell can take a control structure as an argument because
IO () is a value and laziness means it is not run until used. Forth passes an execution token and calls EXECUTE, which is the same idea with the deferral made explicit.unlessZero :: Int -> IO () -> IO ()
unlessZero value action = if value /= 0 then action else pure ()
main :: IO ()
main = do
unlessZero 5 (putStrLn "ran")
unlessZero 0 (putStrLn "did not run"): UNLESS-ZERO ( n xt -- ) SWAP 0<> IF EXECUTE ELSE DROP THEN ;
: RAN ." ran" CR ;
: DID-NOT-RUN ." did not run" CR ;
5 ' RAN UNLESS-ZERO
0 ' DID-NOT-RUN UNLESS-ZEROA genuinely new control structure — one that changes what the compiler does, like adding a loop form — needs an
IMMEDIATE word, and Forth programs do this routinely. That is a step Haskell cannot take without Template Haskell or a preprocessor.Gotchas For Haskell Developers
Composition Reads The Other Way
Haskell's
f . g applies g first, so the composition reads right to left. A Forth phrase runs strictly left to right, in the order the words are written.main :: IO ()
main = do
print ((subtract 1 . (* 2)) 10)
print (((* 2) . subtract 1) 10): DOUBLE ( n -- n ) 2 * ;
: DECREMENT ( n -- n ) 1 - ;
10 DOUBLE DECREMENT . CR
10 DECREMENT DOUBLE . CRBoth columns print the same two numbers, and the two source lines are mirror images of each other. This is the single most common slip for a Haskell reader writing Forth, and it goes unnoticed because both orders compile.
The Built-In Words Are Uppercase
In Haskell an initial capital means a constructor or a type, so case carries meaning. In this Forth case carries none — but the built-in words are spelled in capitals and matched exactly, so
CR is a word and cr is an error.main :: IO ()
main = do
-- Haskell's case rule is semantic: uppercase means a constructor or a type.
let value = 42 :: Int
print value: SHOUT ." forty-two" CR ;
SHOUT
42 . CRMost desktop Forths fold case, so an example typed in lowercase works in gforth and fails here. Defining
: Greet does not define GREET, and nothing warns you that the two are different.Redefining Does Not Reach Back
Defining
SCALE again adds a new dictionary entry and hides the old one from anything compiled afterwards. APPLY-SCALE was compiled earlier and still calls the first one.main :: IO ()
main = do
let scale value = value * 2
let applyScale value = scale value
print (applyScale 10)
let scale2 value = value * 100
putStrLn (unwords (map show [applyScale 10, scale2 10])): SCALE ( n -- n ) 2 * ;
: APPLY-SCALE ( n -- n ) SCALE ;
10 APPLY-SCALE . CR
: SCALE ( n -- n ) 100 * ;
10 APPLY-SCALE . 10 SCALE . CRHaskell has no redefinition at all — a second
let of the same name shadows rather than replaces, and the earlier binding is what earlier code captured. The behavior matches; the mechanism does not, and in Forth it applies to every word in the system.Nobody Is Counting
Haskell cannot express "called with too few arguments" as a run-time bug —
takesTwo 1 is a function, and the type checker rejects printing it. The 99 here is left over from earlier work, the way a cell often is.takesTwo :: Int -> Int -> Int
takesTwo first second = first + second
main :: IO ()
main = do
print (takesTwo 1 2)
-- print (takesTwo 1) -- a type error: no Show instance for a function: TAKES-TWO ( a b -- sum ) + ;
99
1 2 TAKES-TWO . CR
5 TAKES-TWO . CR5 TAKES-TWO supplies one argument, takes the 99 as the other, and answers 104 without complaint. Checking the stack with .S after any word you are unsure of is the habit this produces, because a wrong answer is the only symptom you will get.The Return Stack Is Yours, Carefully
>R moves a cell to the return stack and R> brings it back — the same stack the processor uses for return addresses, which Forth lets you park values on.main :: IO ()
main = do
let (a, b, c) = (1, 2, 3 :: Int)
putStrLn (unwords (map show [a, b, c])): REVERSED ( a b c -- c b a )
SWAP >R SWAP R> SWAP ;
1 2 3 REVERSED
. . . CR🚨 Anything pushed with
>R must be removed before the word returns, or the word returns to the wrong place and the system is gone. Inside a DO loop the index and limit are on that same stack, so R@ there reads a loop counter rather than what you stored. Note also that . . . prints the stack from the top down, which is why reversing it yields the original order.