See also Forth.
I think concatenative programming is best explained when it is contrasted with traditional non-concatenative programming with explicit variables for everything.
Here’s two pseudocode examples. They both perform the same action: "Create a foo from 1 and a bar from foo."
1foobar: Traditional imperative function call version:
var foo = makeFoo(1); var bar = fooToBar(foo);
Instead of explicit variables, concatinative "functions" expect their arguments to simply be provided in a particular order (typically on a stack or modeled on a stack):
1foobar: Concatenative programming with a stack model:
push 1 makeFoo fooToBar
Which is better? Clearly there is no "better". Just looking at the shapes, the first one has much longer lines because we name every intermediate value. The second one requires one more explicit step.
But if we "golf" these into one-liners, it’s interesting to compare the left-to-right order of the concatenative version versus the inside-out function calls of the traditional form.
g1foobar "Golfed" traditional imperative:
var bar = fooToBar(makeFoo(1))
g1foobar "Golfed" concatenative:
1 makeFoo fooToBar
(The push keyword goes away because that’s typically implicit in a concatenative language.)
bar = Foo.make(1).toBar()
Like concatenative programming, any number of values might be "passed" to each method call by being contained in the return objects at each step (also, all of the return objects might be the same object.) But the make() method still has to be passed an explicit '1' because there’s no way to make it implicitly available (other than a global variable or some such - I mean, there’s always a way).
I had a wacky idea for avoiding unneeded stack operations while keeping things more manageable than pure assembler: named-register-passing.