This is an old draft that I discovered when going through my gists. Everything I wrote then is still true, so please enjoy!
This Article has struck a chord with me. I’m not an Erlang-er, but the style he describes of “writing many tiny functions” strongly reminds me of why I’ve come to love Factor.
Drink Beer when Delicious
- “good” or “well written” Erlang functions are very Factor-like
- Tiny functions are easy to understand.
Take the first example: drinking beer when it’s delicious.
This mess:
if
deliciousness(Beer) >= ?EXCELLENT ->
drink(Beer);
true ->
obstain
end
Translates into this, when refactored:
maybe_drink(if_standard_met(deliciousness(Beer), ?EXCELLENT), Beer)
The key realization here is to formalize the “deliciousness comparison” in if_standard_met
. This is the same thought process that happens when ‘factoring’ a word in Factor. For example, this is an ugly but direct translation of the above Erlang if
:
: maybe_drink ( beer -- )
dup deliciousness>> ?EXCELLENT >=
[ drink ] [ abstain ] if ;
Thanks to the terse-ness of Factor’s syntax, the code winds up being very short – just 58 characters! – but due to stack shuffling & symbol noise (>>, ?, [])
it’s downright intimidating. And again, the intent of the code is lost somewhere along the way.
What we want is this:
: maybe_drink ( beer -- beer' )
beer_delicious? [ drink! ] when ;
Wow! How clear! I read that as, “When the beer is delicious, drink!.” A motto to live by, almost as good as the classic Forth bumper sticker FORTH LOVE? IF HONK THEN
J'proteste!
“But,” I hear you protest, “this will lead to half a dozen tiny functions.” “Precisely!” It leads to several small and obviously correct functions (“words” in Factor parlance).
How do we tell if a beer is delicious? Get the ‘deliciousness’ of the beer and compare it to the ?EXCELLENT
standard:
: beer_delicious? ( beer -- beer ? ) #! Is this beer delicious?
... deliciousness ?EXCELLENT if_standard_met ;
How do we compare a score against its standard? For now, greater-than-or-equal-to is fine.
: if_standard_met ( score goal -- ? ) >= ;
>=
is an ordinary word in Factor, a bit like a function in other languages.
How do we drink a beer? Let’s represent “consumed” beer with a boolean flag. So then, to drink a beer:
: drink! ( beer -- beer' ) t >>consumed ; #! set the 'consumed' flag to true
Each is so simple that I can, at a glance, see that they do the right thing.
Implicit left-application
One feature of Factor (and of concatenative languages in general) is implicit left-application, such that:
functional concatenative
f(g(X)) -> X g f
add(1,2) -> 1 2 add
Implicit left-application leads to visually uncluttered code. And it largely eliminates mental juggling of nesting levels.
It does, however, lead to mental stack juggling and a smug sense of “my code is so short!”. And sometimes I need to explicitly deal with the stack using “shuffle words” such as dup
and swap
:
functional concatenive
f(X, X) -> X dup f
f(g(X), X) -> X dup g swap f
Heavy use of dup
and swap
is a code smell, but, in their correct application, they serve to enforce sane stack flow between words.
That means we can translate the ‘refactored’ Erlang version to:
maybe_drink(if_standard_met(deliciousness(B), ?EXCELLENT), B) ->
B dup deliciousness ?EXCELLENT if_standard_met maybe_drink
In practice, dup
is as easy to visually parse as f(X, X)
. The direct translation of maybe_drink
is not pretty to look at, which is why we factor the definition (har har):
: beer_delicious? ( beer -- beer ? )
dup
deliciousness ?EXCELLENT standard_met? ;
: maybe_drink ( beer -- beer' )
beer_delicious? [ drink! ] when ;
I admit: I cheated a little and snuck dup
into beer_delicious?
to keep maybe_drink
short, clear, and beautiful. Considering the original, this makes sense:
maybe_drink(f(B), B)
beer_delicious?
provides both the flag and argument for when
conditional. Factor’s stack-effect declaration makes that explicitly clear:
: beer_delicious? ( beer -- beer ? ) ...
#! Accepts a beer, and returns the same beer with a flag ('?')
Those stack parameters can be named anything, but convention is that ?
means a boolean value. Another convention is appending a tick or “prime” indicator when the word alters the object:
: maybe_drink ( beer -- beer' ) ...
#! This accepts a beer and returns a modified beer.
Read that as “accepts beer, returns beer prime” — beer'
is either the beer, consumed, or the original beer, untouched.
(Because this word can thirstily modify the beer
there’s one more convention to follow: it should be called maybe_drink!
)
Summary
- Small is beautiful.
- Small is easy to understand.
- Small definitions lead to clarity of thought.
And above all:
- When the beer is delicious, drink.