Concatenative style programming languages are, indeed, powerful and concise. However, they are generally consider to be “read only” languages. The LISP family of languages (including Clojure) shares this same fate, and for much the same reason. The lack of boilerplate verbiage grants power, but hinders readability. Since programmers spend far more time reading code than writing it, this is not a good thing.
The Problem
Compare and contrast the following single line forms:
Concatenative:
3 2 > {“3 > 2 is true”} {“3 > 2 is false”} if printlnClojure:
(println (if (> 3 2) “3 > 2 is true” “3 > 2 is false”))Python:
print(“3 > 2 is true” if 3 > 2 else “3 > 2 is false”)Which one is the easiest to read? Now realize that the one-liner isn’t the easiest form of a Python if statement. Let’s split them up across multiple lines and make them as clear as possible.
Concatenative:
3 2 >
{“3 > 2 is true” println}
{“3 > 2 is false” println}
ifClojure:
(if (> 3 2)
(println “3 > 2 is true” )
(println “3 > 2 is false”)
)Python:
if 3 > 2:
print(“3 > 2 is true”)
else:
print(“3 > 2 is false”)The Solution
There are a pair of simple constructs that can make a concatenative language much easier to read. These are the single-word comment and the function-call syntax.
Let’s start with this. It’s not terrible, but could obviously be better.
3 2 > "3 > 2 is true" "3 > 2 is false" if println
We can visually pick out the conditional in a block. Remember, (blocks) are immediately unpacked for evaluation. {Quotes} and [data] are not. Lines and indentation help, as well.
(3 2 >)
{“3 > 2 is true”}
{“3 > 2 is false”}
if
printlnSingle word comments: The commenting word is attached to double backslashes. No space between them is allowed, or else it becomes a comment to the end of the line.
\\if (3 2 >)
\\then {“3 > 2 is true” println}
\\else {“3 > 2 is false” println}
ifFunction-call syntax: This is a parser hack. It takes the instruction attached to the front of a block (and only a block) and moves it to the last position inside the block. For example, the following all evaluate to 3.
1 2 +
1 (2 +)
(1) (2 +)
(1) (2) (+)
+(1 2)
1 +(2)
(1) +(2)
(1)+(2)Getting back to our sample program, we can now write it like this:
if(>(3 2) “3 > 2 is true” “3 > 2 is false”) println
becomes:
((3 2 >) "3 > 2 is true" "3 > 2 is false" if) printlnEven better, mix these two special forms together for maximum clarity, along with separate lines and indentation. A little code duplication can also help ease readability.
if( 3 >(2) \\ This becomes 3 (2 >) which gets evaluated as 3 2 >
\\then {"3 > 2 is true" println}
\\else {"3 > 2 is false" println}
)There are many occasions when you need to do some complicated computation to base your branching code on. Or the branch might occur based on some factor you don’t know ahead of time, like user input or a database read. That’s fine. Just rewrite your program using the function-call form and indentation to your advantage.
print("Enter a number: ")
getNum
(dup * 100 >)
if(
\\then "The square of your number is greater than 100."
\\else "The square of your number is not greater than 100."
)
println
Partial Application
The last program looks an awful lot like partial application. That’s because it is. Partial application is trivial in a concatenative language. It’s how all the functions work, because they expect to have their arguments waiting for them on the stack.
We can define an increment function like so:
define(
{1 +}
\\as "inc"
)
2 inc \\ --> 2 (1 +) --> 3Want to make a custom adder? That’s only a bit more complicated.
2
{+} curry "addTwo" define
1 addTwo \\ --> 1 (2 +) --> 3Want to make a custom adder generator? That requires just a bit more thought.
\\ expects: #number "name"
define(
{ { {+} curry} dip
\\ the stack is now: {#number +} "name"
define
}
\\as "adderGenerator"
)
3 "addThree" adderGenerator
1 addTwo addThree \\ --> 1 (2 +) (3 +) --> 6
"addSix" adderGenerator \\ 6 is already on top of the stack
4 addSix \\ --> 4 (6 +) --> 10
\\[ Without the comments and formatting, this function-defining function definition is just:
{ { {+} curry} dip define} "adderGenerator" define
\\]Try doing that in most other language styles.
Multiple Asides
The writing of functions without variables is called point free programming.
Functional composition is sometimes referred to as railway or pipeline oriented programming.
In concatenative programming, it’s easy to associate data with code using quotes and combinators. Language-level closures are largely unnecessary.
No comments:
Post a Comment
I reserve the right to remove egregiously profane or abusive comments, spam, and anything else that really annoys me. Feel free to agree or disagree, but let's keep this reasonably civil.