This is an old draft that I discovered when going through my gists. Everything I wrote then is still true, so please enjoy!
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.
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
: 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
"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
: 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.
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
functional concatenive f(X, X) -> X dup f f(g(X), X) -> X dup g swap f
Heavy use of
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
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
beer_delicious? to keep
maybe_drink short, clear, and beautiful. Considering the original, this makes sense:
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
- Small is beautiful.
- Small is easy to understand.
- Small definitions lead to clarity of thought.
And above all:
- When the beer is delicious, drink.