Here's another way to look at it. The stack is the global state of the program; and this global state is passed as an argument to every function. By an unkind interpretation, a stack language is the opposite of functional; every function isn't just mutable, it mutates the world.
In a shell pipeline of 'a | b | c | d', I can see the dataflow clearly. 'c' never sees the output of 'a' directly; it must be mediated by 'b'. But for a stack, 'a b c d', I have no idea if c uses the value a or not, unless I know the definition of b and how many items c consumes.
When reading stack code, I find myself mentally inserting parentheses to see how far back each operation reaches; for higher-level operators, I find myself wanting to insert comments describing the shape of the code being implicitly constructed. I need to see the "tree" (though frequently a dag) shape.
This isn't different in complex infix expressions. There too, parentheses are often needed for clarity - and good programmers use them even when not necessary - and for very complicated expressions, I'll spread them out across multiple lines, with indentation to make the tree explicit, where performance or similar concerns prevents writing out the expression in a more long-hand fashion.
That's one reason I've found the pipeline metaphor not great as a way of introducing concatenative languages. To me, the shell pipeline, which has a data-in/data-out streaming kind of flavor, generalizes (with multiple input/outputs) into dataflow languages, not into concatenative languages. There are definite connections between the paradigms, but imo the stack-manipulation is a different thing not present in stream/pipe based languages. The graphical music language Pd literally draws the pipelines as wires connecting input and output ports on functions (inspired by physical patch cables), which seems much closer to what I'd expect a generalized version of shell pipelines to look like.
Statically typed concatenative languages are precisely dataflow languages. Dynamically typed ones aren’t, because they can have dynamic stack effects. But the diagrams in my article look just like PureData programs for a reason.
Dataflow languages are what I'm most interested in these days when it comes to programming languages and I've always liked the symmetry between concatenative languages and pure dataflow languages (visual ones or just stream/flow based textual languages).
I've also been doing a lot of Max/MSP programming lately and really love working in its "passing messages along patch cables" system, but am frustrated by its lack of higher order objects and lack of aggregate data types (hell, it won't even let me create a list of lists).
What I really want is a Max/Pd-like language with richer set of data types (algebraic types with pattern matching would be a good start) which can be trivially and unambiguously converted between visual and textual representations. A concatenative language may make this a lot easier.
I think you’re really going to like Prog, then. It’s concatenative but focused on ADTs and pattern matching. Abstraction is easy thanks to quotation. I have some neat ideas in mind for an editor, as well, that would do just what you say—display a flow diagram of a program or selection as you edit. Concatenative languages are indeed, as the bland Wikipedia article puts it, “highly amenable to algebraic manipulation” of this sort.
It's an applicative language created especially for audio DSP. It seems to bridge the gap you describe by providing dataflow combinators and a graphical representation for programs.
This is exactly my issue with stack-based languages as well; i.e., '(f g h)' vs. '(f (g h))' are pretty easy to distinguish, but both of those would be an identical 'h g f' in a stack-based language.
On the other hand, in an application language, (((f .) .) . g) (for a composing a unary function for with a ternary function g) gets confusing as well.
In a shell pipeline of 'a | b | c | d', I can see the dataflow clearly. 'c' never sees the output of 'a' directly; it must be mediated by 'b'. But for a stack, 'a b c d', I have no idea if c uses the value a or not, unless I know the definition of b and how many items c consumes.
When reading stack code, I find myself mentally inserting parentheses to see how far back each operation reaches; for higher-level operators, I find myself wanting to insert comments describing the shape of the code being implicitly constructed. I need to see the "tree" (though frequently a dag) shape.
This isn't different in complex infix expressions. There too, parentheses are often needed for clarity - and good programmers use them even when not necessary - and for very complicated expressions, I'll spread them out across multiple lines, with indentation to make the tree explicit, where performance or similar concerns prevents writing out the expression in a more long-hand fashion.