top | item 3582261

Why Concatenative Programming Matters

219 points| evincarofautumn | 14 years ago |evincarofautumn.blogspot.com | reply

40 comments

order
[+] barrkel|14 years ago|reply
The problem with stack languages for me has always been in typing. I find it difficult to track the exact stack effects of each word (function), particularly if these functions are pushing, popping and applying functions that themselves modify the stack. Even when dealing with something as simple as optimized x87 code, I would annotate all the FXCH etc. to keep track of the bits of the arithmetic expression on the stack, as I tried to keep multiple operations in flight.

Yes, I know Christopher Diggins' Cat is statically typed. The problem isn't in compiler diagnostics, it's in readability.

In a stack language, an expression as simple as 'a b c d' is cryptic. If c consumes two arguments, in applicative terms it would be d(c(b, a)); if b consumes one argument and c only one, then it would be d(c(b(a))); if d consumes two arguments, b one, and c none, then it would be d(c, b(a)). And this ambiguity makes it very hard for me to read the code unless I'm intimate with all the individual words. You have to understand everything to understand anything.

Unfortunately, I suspect that concatenative languages tend to derive their power from this ambiguity, because the lack of specificity leaves considerable flexibility for what the words can do with the stack.

So my considered opinion for now - and this article doesn't really change that, it is mostly a tutorial - is that stack languages are best as a compiler target rather than a human source form.

[+] dkersten|14 years ago|reply
I always liked this (slightly paraphrased) quote:

    Idiomatic Factor does not (explicitly) use the stack.
That is to say, in Factor, a popular concatenative language, you tend to use higher order functions and high level combinators instead of manually shuffling stuff around on the stack. Sure, from time to time you still need to move stuff into the right place, but its easy to keep track of the stack if you only need to consider two or three items. At least, that seems to be the theory - I have not used Factor in a number of years so don't know how well that holds up in practice.

Also, Factor does allow you to use local (named) variables and infix syntax when it makes more sense to do so. Concatenative languages do not need to be unreadable at all and for some things I think the concatenative code is actually more naturally readable.

"If c consumes two arguments, in applicative terms it would be d(c(b, a)); if b consumes one argument and c only one, then it would be d(c(b(a))); if d consumes two arguments, b one, and c none, then it would be d(c, b(a))."

'a b c d' in a concatenative language is akin to 'a | b | c | d' in a shell script (where a stack is being threaded from function to function) or 'd(c(b(a(STACK))))' in most languages where each function has a type 'Stack func (Stack s)'. It doesn't change depending on how many arguments a function takes off the stack - its always 'd(c(b(a(STACK))))'. Of course, since literals are essentially functions which push their value onto the stack, what you say is true if a, b and c are literals. I don't find this nearly as confusing as what it sounds like from your comment.

In code like '1 2 + print', all the words you see are functions. 1 pushes the value of 1 onto the stack and so on. This is interesting because you can easily factor out any part by replacing it with a new word that calls the replaced word:

    :A 1 2 + print
    
    :add2 2 + ;
    :B a add2 print
A and B are the exact same.

This is also interesting: http://concatenative.org/wiki/view/Concatenative%20language

[+] RodgerTheGreat|14 years ago|reply
The ambiguity produced by stack effects is also paralleled in most infix procedural languages. C++ recognizes 18 levels of operator precedence[1] with two types of associativity when evaluating expressions, and any of those operators can be overloaded to produce side effects for different combinations of types. Sometimes this behavior is intuitive, other times it can be deeply confusing.

For what it's worth, my experience is that practice and good factoring can make concatenative languages much less cryptic. In Forth, the ideal size of a procedure is about a line, and procedures that take more than three arguments are considered a "code smell" that suggests a different factoring might be more appropriate. When you only care about the top few stack elements within any given definition there's a lot less to juggle mentally.

[1] http://en.cppreference.com/w/cpp/language/operator_precedenc...

[+] evincarofautumn|14 years ago|reply
That ambiguity is the “uniform composition” I was talking about, which is the essence of the paradigm. But I agree that it can be a lot to keep in your head if you think about everything as a stack. That’s why I’m offering applicative syntax in Prog, because it’s far clearer in many cases than the concatenative equivalent.

However, giving Prog a concatenative basis lets me do all kinds of interesting things much more easily than if I were to work from lambda calculus. With the syntactic issues taken care of by sugar, it’s a net gain. I hope you’ll see your way to trying it out one day.

[+] sklipo|14 years ago|reply
I agree, but I think that with a great IDE for stack languages which would track the stack and the stack effects of functions, and display it all in a convenient way, most of the problem would be solved. It would be hard to read code without it, but I'll argue that the same could be said about syntax highlighting. And honestly, I think reading this kind of code can become natural with practice.
[+] tailrecursion|14 years ago|reply
This article introduces concatenative langauges, but it doesn't explain why they matter.

In my work, I found that the lack of syntactic marking for arguments of function calls is a devastating drawback. Coming back to my own function definitions a week after writing them, I spent minutes unpuzzling the code. I could use every trick and write the function in the most elegant way, using combinators in the style of Factor, and that made it worse. But writing it out with local bindings also didn't help. As soon as you write a nested expression, you start to lose the sense of which values are being passed where.

gcd n1 n2 n2 0 = if n1 else n2 n1 n2 mod gcd

gcd zero if else "mod" plus swap gcd

factorial 1 interval product

I was designing a Forth-like functional language with generic functions, and for a long time I felt that the lack of markings didn't matter. I thought that one could understand the code "well enough" without knowing exactly where the arguments were going. There is something to this argument, and there's a lot we can do to make code say what it does. But sometimes the author just had to implement something, and technicalities arose, and when you go to read it, the code doesn't "read" and you have to bring your own intelligence to figure out what it does -- it won't tell you. In these cases, explicit argument marking is indispensable.

In Forth-like languages, simple things are wonderful and elegant. There is no other shorter way to write than point-free. And, writing with combinators is satisfying, and it can make the "essence of the function" more evident. But unless you severely constrain your style to ultra short definitions, avoid nested expressions, and use local variables profusely, you're constructing puzzles for yourself.

It's almost like the concatenative style is too optimal. You have to back up a step in beauty in order to get a practical tool.

[+] gruseom|14 years ago|reply
The two best-known features of Chuck Moore's style -- extreme simplicity and extreme factoring -- must be how he avoids the dangers you describe. I wonder if these qualities go beyond stylistic choice and are actually necessary if one is to build real systems in Forth. That might explain why programmers who come from other languages tend not to stick with it. Your story is reminiscent of others I've read in the past (most prominently http://www.yosefk.com/blog/my-history-with-forth-stack-machi...) that talk about being in love with the Forth model and then abandoning it because it got too complicated to make it do what they want.

Makes me wonder if trying to do what you want in that model is a recipe for disaster; instead, maybe you have to let the model tell you what you can do: if it's simple you get to do it and if it's not you don't. Chuck would see that as an advantage, but most programmers wouldn't.

Edit: the article I linked to talks about that at length. His conclusion is that if you're going to build systems in Forth, you need the freedom to shrink or morph the problem into something that can be solved simply in Forth. If you don't have that freedom (e.g. because people are feeding you inflexible requirements), it probably won't work...

[+] cromwellian|14 years ago|reply
If this article is a homage to the famous "Why Functional Programming Matters" paper, I think it failed in a major way. The Why FP Programming Matters paper began each section with practical programming problems that FP solved elegantly. It built up the solutions piece by piece and showed at each step, how easy they were to comprehend, extend, and abstract.

The FP paper is very effective in making people who are hard core imperative programmers want to try FP. But the CP article here doesn't really IMHO elicit the same desire.

You should try writing another one with some practical examples.

[+] evincarofautumn|14 years ago|reply
It wasn’t really supposed to be, but sure, I gladly will.
[+] Drbble|14 years ago|reply
The body does not deliver on the promise of the title, but does raise the question.

The answer seems to be that concatenative languages give you the power of functional languages using a low-level nearly absent syntax (like Lisp). I guess that's useful as way to demystify the compilation process (like how assembler and CS demystify how the CPU actually gets work done, as contrasted against magic like Ruby), and matters if you want to understand or write a VM, or reason about a program in a completely unbiased way that (for better or for worse) has no cognate to human language (but may be an elegant non-linguistic mechanical language).

For general purpose programming, I prefer a little syntactic sugar. Humans seem to have an instinct for language, and it is helpful to leverage that instinct when writing and ituitively grokking programs, if not for constructing formal proofs.

[+] evincarofautumn|14 years ago|reply
Agreed on all points, actually. My language Prog, though concatenative to make my life easier, has syntactic sugar for yours.
[+] dustingetz|14 years ago|reply
minimalistic syntax isn't necessarily the holy grail. We can't pinpoint exactly why Clojure is hockey-sticking but Common Lisp didn't[1]. But Clojure took a minimalistic language, then made a few very tactical departures from minimalism - common data-structures have native syntax encouraging people to use more than just lists, and the native data structures are immutable encouraging a functional style. from this we learn that minimalism is a great base, but the langage forms opinions about what code should look like via its syntactical choices, and these opinions shape its community, libraries and ultimately its success. language designers must take this into account.

with that said, you can do "concatenative" programming in any language supporting a functional style (python, ruby, clojure, haskell, javascript), and some of these languages (clojure, haskell) encourage or force functional style. clojure and haskell have different opinions about how this should be done, but they are opinions, not gospel, and each has their use for different problem domains and personalities.

the author makes some assertions that a "concatenative" language, one that encourages a concatenative style, are better than functional languages. i don't think these opinions were adequately backed up at all - i'm not just skeptical, i sort of think he's wrong. both clojure and haskell strongly encourage composition over application. these languages already have concatenative opinions. i don't really see the fuss here, i don't see the need to introduce more metaphors into an ecosystem which is already unfriendly to noobs. what we need is a language as noob friendly as python which has functional opinions. ruby got noobs using lambdas without realizing it, but ruby also has some weird opinions about magic which the functional community frowns upon. there's a clear niche here.

[1] (edit for gruseom) but we can try to learn from those who are successful, especially those who failed a few times first. Martin Odersky has written all about his failures leading to Scala, and Rich Hickey talks about what he thinks made Clojure successful all the time.

[+] gruseom|14 years ago|reply
We can't pinpoint why Clojure is hockey-sticking but Common Lisp didn't.

That's exactly right. But then you go on to do just that ("from this we learn...") in a bogus way. We don't know what drives these things. All we have are competing arguments from personal preference.

[+] evincarofautumn|14 years ago|reply
I never said concatenative style is better than functional style (outside of very specific examples), so the person you think is wrong doesn’t actually exist. Anyway, I agree all around. Although you can’t say for certain what makes a language popular or not, I think that picture of Clojure is pretty accurate.

And I would love to see a functional language as powerful and carefully designed as Haskell, without all of that language’s minor annoyances and barriers to entry. Even though the Haskell culture is great, it’s unfortunate (to paraphrase SPJ) that they picked the scary term “monad” instead of “warm fuzzy friendly thing”.

[+] gnosis|14 years ago|reply
There's one simple reason why Clojure is getting so much momentum: Java.

If it weren't for it sitting on the JVM and having access to all those Java libraries, Clojure would have been dead in the water.

[+] erichocean|14 years ago|reply
Here's a practical example:

I have found a concatenative language (Factor, specifically) to be extremely useful for use in a workflow system. In particular, since it's not applicative, there's no API between "words", so it's damn near trivial to combine workflow sequences together -- no API is necessary. Each sequence of words (think of it as a workflow) can consume and or produce as much data as needed -- again, without any API coordination whatsoever.

[+] DasIch|14 years ago|reply
As much as I like the basic idea, I fundamentally dislike that the control flow becomes implicit. In order to have any understanding of what is happening you need to know the effect each function has on the stack and you need a mental model of the stack or at least it's current state.

In an applicative language this is explicit and it is probably the main reason for the fact that it is relatively easy to understand code in any procedural language once you've learned one.

[+] dustingetz|14 years ago|reply
in a purely functional language, you don't need to think about the stack any more. your code expresses its dependencies via composition and the order in which things happen becomes the compiler's concern. you don't need or want to have an understanding of what's happening in an imperative sense. bam, a whole class of bugs made impossible. [edit: you're right though, if you actually do need to understand things in an imperative sense, e.g. for performance optimizations, things get complicated.]
[+] _delirium|14 years ago|reply
I don't see the control flow as more implicit than in applicative functional languages. I agree control flow is more explicit in procedural languages, but in, say, Haskell, control flow seems implicit in approximately the same way as in Factor, in that what you would do via explicit control flow in C, you do instead via functions/words that do control-flow-like things, like "map" or "iterate".
[+] abecedarius|14 years ago|reply
I once made a variant of Backus's Turing-lecture language FP, where to compose functions you wrote 'f g' instead of the original 'g o f'. I'm curious if people into concatenative languages would call this one of them -- it satisfies the definition in the article, but there's no stack, so I'd guess not. It's more like point-free Haskell with some notational support for the style.

(On second thought, this post's title echoes Backus's. Maybe they would include FP.)

[+] adrusi|14 years ago|reply
The way of reasoning about `2 3 x` that it presents is extraordinarily confusing, I can't really comprehend it as it is described.