Long, long ago I wrote a Forth of OS/2 out of spite. I've had a long standing interest in the language. It's been my opinion for a while that Forth is a less powerful language than LISP, even though they initially appear to just be the opposites of each other in terms of argument handling.
The thing that makes Forth and other similar languages write-only is that you can easily lose track of the variables when you are forced to toss them all on a stack. When you add the ability to use local variables, you get some of that power back, but then you've added a huge impedance mismatch with the rest of the system.
As much as I hate all the () in Lisp, it's the source of much of its power.
I write code in both and they seem pretty much equally powerful to me.
There are two big differences for me:
1. If I look at Forth code I wrote a month ago I have no idea what it's doing. This is never a problem with Lisp.
2. I can -- and have -- written a complete Forth compiler bootstrapping my way up from assembler. Sounds like you've done the same thing. I could never bootstrap a Lisp compiler that way.
> If I look at Forth code I wrote a month ago I have no idea what it's doing.
Sometimes when we are learning a language, we plateau in our learning for so long we think we are done, however I assure you that other people can read Forth code written months or years ago, even written by other people, and can read it.
That's not you, at least not yet, and you should be cautious with your certainty about things you just don't know about.
Writing code is substantially easier than reading it, but reading is when it is possible for us to know the language.
> I could never bootstrap a Lisp compiler that way.
Don't be so hard on yourself: You can, but you would have to change and learn in order to do it.
Experienced Forth programmers do not tend to make functions with lots of stack-based temporary references, but leverage the hyperstatic nature of globals which are effectively keyword arguments for subroutines.
Pre-CL, lisp programmers use dynamic variables in a similar way (especially around the reader and writer), and whilst this carried over into keywords in CL, the vestiges of the defaults being the old dynamic variables remains.
It has to be something about familiarity, because for me it's the opposite. I find that nested parens in Lisp make the code pretty hard to read. It's difficult to pinpoint where the processing begins. I also find myself constantly scanning left and right for parens boundaries. It's exhausting.
In Forth, you read your word from left to right, "playing" stack effect in your head, and you always know where you're at processing wise.
Pretty simple, but I guess one must have inclinations towards thinking about the generated native code underneath rather than thinking in terms of symbols.
If you see write-only Forth programs, that's probably because they are poorly written, or maybe you are not familiar enough with Forth to read them.
> is that you can easily lose track of the variables when you are forced to toss them all on a stack
You don't "have" to keep everything on the stack in Forth any more that you "have" to allocate everything on the heap in C/C++.
A Forth programmer who is not a dilettante will look for the data which is used throughout the program (e.g. a file or device handle, a memory address) and put it in a (global) variable.
This eliminates most of the stack juggling and your definitions generally use no more than 2 arguments, so it's easy to tell what they do by looking at them.
You realize you are talking about something you have never seen, right?
I do. Maybe my ten thousand lines of Forth were barely readable. But not anymore. I have dozens of scripts meant to be throwaways, not something perfect and beautiful meant to published on Github, some of them I actually reused No Problem.
And I'm not even a professional Forth programmer; it only has been my go-to language for scripts, prototypes, utilities for a solid decade now.
Maybe your intuition tells you that it "has" to be write-only. Full disclosure: I became interested in this language because I thought there's no way it could be viable - forget about "write-only" and "readability", I was already over those fallacies at that time. But on the other hand, there was contrary evidence [1]. So I gave a honest and earnest shot at it.
What I learned, among other things, is that most of the work is about going against the habits and ways of thinking that you learned from mainstream languages. Of course it is not something one can learn by stagnating in the comfort zone of Algol++ languages.
the thinking relies on the "sufficiently smart XYZ".
similar to
- true C++ developers do not write bugs
- true agile practitioners build paradise on earth (i jest)
- good enough AI coder are vastly more efficient than manual coders
etc.
when somebody's first answer to a problem/inquiry is "you're not good enough or else you would see the lights" it raises alarms in my head.
that said, my first steps in programming were forth (and hp RPL). so i got some familiarity, read code, wrote code, wrote basic interpreters too (for learning purpose). forth is cool and has its uses.
This is mostly a problem of concatenative language design. There is nothing preventing function definitions from having type signatures that are statically checked. In my concatenative language compilers, I always provide a function that causes the compiler to halt and print out all of they types on the stack at the call site during compilation as well as a runtime function that prints out the runtime values. I have found this to be quite effective for debugging. Because I don't support global variables, these functions give a complete view of ALL of the live values at any point in the program, which is quite hard in most languages.
Local variables are also not necessary to have reasonable ergonomics, even in mathy contexts. Instead, the language can define special operators that access fields on the stack by type name and move or copy them to the top of the stack. For some reason, in the math example, the author deliberately added an unnecessary parameter that had to be dropped and put the arguments in the wrong order. Cleaning up the signature, there are several reasonable ways to solve the problem in a concatenative language with the features I've described.
type n number
type y n
type x n
def op_math_example [(y x) -> (n)] \\x * \\y \\y * + \y abs -
where \FOO moves a value of type FOO to the top of the stack and \\FOO copies the value to the top, leaving the original where it was. Yes, the \\ is a little ugly, but these should generally be used sparingly.
Even if we restore the original signature and use the author's syntax, there is a much cleaner solution to the problem without the move/copy operators.
def sq dup *
def op_math_full drop swap sq over sq + swap abs -
This is extremely cryptic. I suspect most forth programmers would have added a stack signature comment, but it is even better if the compiler statically verifies the signature for us, which is how the language I am describing differs from vanilla forth (where stack comments also have the risk of being wrong in an evolving system). If we restore the type system, it becomes:
type n number
type x n
type y n
type z n
def sq [(n) -> (n)] dup *
def op_math_impl [(y x) (n)] sq over sq + swap abs -
def op_math_full [(x y z) -> (n)] drop swap op_math_impl
Of course it is still not as obvious to the reader what op_math_full actually does as (y * y) + (x * x) - abs(y). But in practice it would have some name like projection_size so once it has been written correctly, it doesn't really matter that it is a bit obscure to read at a glance. You also get really good at simulating the stack in your head and they type signature makes this much easier. Still, when you are confused, you can always add stack debugging like so:
def projection_size [(x y z) -> (n)] drop swap PRINT_STACK sq over sq + swap abs -
1 2 3 projection_size
and the output would be something like:
({ y 2 } { x 1 })
Writing programs in this style is very nice so long as you have enough tooling to facilitate it.
I have only used concatenative languages that I have implemented myself, however. I have read quite a bit about forth, but for me, there are clear problems with vanilla forth that are solved for me with the meta operators like [, [[, \ and \\ as well as good debug tools like PRINT_STACK above. Forth was an incredible discovery for its time. We can do a lot better now.
Hi, as someone also fiddling around with a concatenative toy language, I wanted to ask if any of your languages have a public repository somewhere? You seem very knowledgeable on the topic and your descriptions made me interested.
Do most people tend to conclude that concatenative languages are absolutely an intellectual curiosity for any nerd who knows RPN and thought "what if we kept going?", but kind of fail in larger-scale production contexts?
(Yes, I'm aware of PostScript... Seems to be one of the sole exceptions here... And, uh, PDF... of course...)
The odd thing is that RPN is actually pretty intuitive for people. It's the "language" of cash registers! I had never used cash registers, and I lent my HP rpn calculator to my girlfriend to use. I said it's a bit weird, do you need help.
She gave me the dirtiest "are you mansplaining me" look.
Using the HP was perfectly natural for her as she had used cash registers.
> It is considered good functional programming style to write functions in point-free form, omitting the unnecessary mention of variables (points) on which the function operates.
This is wrong.
It may be considered good programming style in certain small use-cases when a small local composition is clearer. But as a general software engineering principle, the exact opposite is true: It is considered good style to break larger constructs into smaller pieces and give them meaningful names.
There's no reason that function parameters should be an exception to that and in practice, names for function parameters are often some of the most critical names for understanding a system.
I think concatenative languages are really cool as a mental exercise in getting a lot of power out of something simple. But as far as why they aren't used in practice. I think it's largely because they are a bad idea carried to its logical extreme.
I’m pretty sure the programmability of PostScript was a mistake. Adobe clearly thought that because most of it was removed from PDF.
I was working in sales/support/marketing for printer companies in the eighties and people were buying PostScript printers for the scalable fonts, device independence and compatibility. The most common objection was performance which was somewhat related to the programmability.
We used to supply the red and blue books with each printer but I don’t remember anybody breaking the seal on them.
Having said all that I have spent the last year writing a PostScript interpreter.
I’m not sure I’d call PDF concatenative, you certainly can’t cat two PDF files together to make a merged document. There are header and footer sections to deal with.
> I’m pretty sure the programmability of PostScript was a mistake. Adobe clearly thought that because most of it was removed from PDF.
I do not agree. I think the mistake was using PostScript as the output document format, not its programmability. PostScript should be the input format and not the output format.
I think PDF has many problems and is badly designed in many ways, though.
PostScript is a bit like .Net CLR or WASM: while widely used, it's basically never written by humans directly.
Forth is a wonderful way to build a high-ish level self-contained programming environment on very low-resource hardware, like a 8-bit MCU. But thinking in terns of the stack is useless mental gymnastics, this is something a machine should do instead.
People do write in PostScript directly (and I have seen .NET and WASM code written directly, too). I use PostScript directly and so do some other people.
Absolutely. Years ago writing programs for *BSD/Linux, PS was the natural, most direct way to implement printing to printers equipped with a PS interpreter.
Fortunately the PS language was very well documented. That made writing PS pretty straightforward, at least for the reasons I was using it. Curiously other concatenative languages have been harder for me to grasp. Maybe that's because I regarded PS as a specific-use tool vs. a general purpose language.
If nothing else PS showed the value of excellent documentation. Lack of it probably accounts for many software project failures, particularly in the open-source world.
You can also run PostScript programs on the computer; you do not need a PostScript printer.
> Maybe that's because I regarded PS as a specific-use tool vs. a general purpose language.
In my opinion, it is both. Many of the programs I write in PostScript do not involve a printer at all.
> If nothing else PS showed the value of excellent documentation. Lack of it probably accounts for many software project failures, particularly in the open-source world.
I also find a problem with many programs that do not have good documentation. When I write my own, I try to provide documentation.
From what I can tell the mental gymnastics are leaning how to think in terms of Forth instead of how to think in terms of the stack, which is the stack effect and not the stack itself. You only think about the stack itself with your base words (when stack and stack effect are essentially the same), after that it is the stack effect; a single word may push and pop thousands of times but you don't need to think about that, just make sure that it leaves the stack (or stacks) in the state you want it (them). Once you have built up a few layers from the base words I find it more like having a single variable which you store the current state in than a stack, so functional stateless programming but with a tiny dash of state?
There are mental gymnastics but they are far from useless and not just thinking in terms of the stack. I don't quite have it down yet but I am getting closer and can see the sense in it, I haven't quite figured out how to plan and structure a program in Forth.
Effects are pretty smoothly described by monads; there's an entire well-respected language built on top of that.
One of the problems of the purely applicative approach is that you get a nice algebra for describing your results, including the sequence of effects, but reasoning about the resources spent by the computation becomes harder.
monads were certainly a clever fix for the difficulties otherwise caused by passing around the world as a value.
I might argue that laziness makes haskell something different than purely applicative.
It's not just pure values being passed around. It's a language built atop suspended self-caching invocable thunks getting knotted up and then firing off in chain reaction.
main = do
let
hello = foo world
world = bar hello
putStrLn $ show hello
putStrLn $ show world
where
foo v = [v]
bar v = length v
Modern programming is. But look at assembly, which is what actual machines do. And you can write assembly with tons of gotos and not a single function in sight.
There's really no such thing as a 'function' at the assembler level. There are pure jumps and there are jumps that also perform side effects on a register conventionally called a 'stack pointer' (and on the memory that register points to) but no true 'functions.'
This is roughly the point Steele is making, in addition to his point that the stack pointer side effects are often unnecessary (i.e. tail jumps).
Implementing a stack pointer in hardware is merely a legacy hack: Before people figured out continuations they didn't know any better.
If you look at the computer (registers, cache, RAM, storage, etc) as one whole, every assembler instruction is just a function that takes in the state of the computer and returns a new state. Multiple assembler instructions is then using function composition.
Modern machines do. But the dominance of imperative assembly is more about historical momentum and engineering tradeoffs than fundamental necessity. We don't have to go all-in on Von Neumann.
Long, long ago I wrote a Forth of OS/2 out of spite. I've had a long standing interest in the language. It's been my opinion for a while that Forth is a less powerful language than LISP, even though they initially appear to just be the opposites of each other in terms of argument handling.
The thing that makes Forth and other similar languages write-only is that you can easily lose track of the variables when you are forced to toss them all on a stack. When you add the ability to use local variables, you get some of that power back, but then you've added a huge impedance mismatch with the rest of the system.
As much as I hate all the () in Lisp, it's the source of much of its power.
I write code in both and they seem pretty much equally powerful to me.
There are two big differences for me:
1. If I look at Forth code I wrote a month ago I have no idea what it's doing. This is never a problem with Lisp.
2. I can -- and have -- written a complete Forth compiler bootstrapping my way up from assembler. Sounds like you've done the same thing. I could never bootstrap a Lisp compiler that way.
> I write code in both ...
> If I look at Forth code I wrote a month ago I have no idea what it's doing.
Sometimes when we are learning a language, we plateau in our learning for so long we think we are done, however I assure you that other people can read Forth code written months or years ago, even written by other people, and can read it.
That's not you, at least not yet, and you should be cautious with your certainty about things you just don't know about.
Writing code is substantially easier than reading it, but reading is when it is possible for us to know the language.
> I could never bootstrap a Lisp compiler that way.
Don't be so hard on yourself: You can, but you would have to change and learn in order to do it.
LISP has more names (for function arguments), which convey more information.
I don't think you know what you mean.
Experienced Forth programmers do not tend to make functions with lots of stack-based temporary references, but leverage the hyperstatic nature of globals which are effectively keyword arguments for subroutines.
Pre-CL, lisp programmers use dynamic variables in a similar way (especially around the reader and writer), and whilst this carried over into keywords in CL, the vestiges of the defaults being the old dynamic variables remains.
It has to be something about familiarity, because for me it's the opposite. I find that nested parens in Lisp make the code pretty hard to read. It's difficult to pinpoint where the processing begins. I also find myself constantly scanning left and right for parens boundaries. It's exhausting.
In Forth, you read your word from left to right, "playing" stack effect in your head, and you always know where you're at processing wise.
Pretty simple, but I guess one must have inclinations towards thinking about the generated native code underneath rather than thinking in terms of symbols.
> Forth and other similar languages write-only
If you see write-only Forth programs, that's probably because they are poorly written, or maybe you are not familiar enough with Forth to read them.
> is that you can easily lose track of the variables when you are forced to toss them all on a stack
You don't "have" to keep everything on the stack in Forth any more that you "have" to allocate everything on the heap in C/C++.
A Forth programmer who is not a dilettante will look for the data which is used throughout the program (e.g. a file or device handle, a memory address) and put it in a (global) variable.
This eliminates most of the stack juggling and your definitions generally use no more than 2 arguments, so it's easy to tell what they do by looking at them.
well this paragraph pretty much guarantees that most of the forth code out there is write only.
You realize you are talking about something you have never seen, right?
I do. Maybe my ten thousand lines of Forth were barely readable. But not anymore. I have dozens of scripts meant to be throwaways, not something perfect and beautiful meant to published on Github, some of them I actually reused No Problem.
And I'm not even a professional Forth programmer; it only has been my go-to language for scripts, prototypes, utilities for a solid decade now.
Maybe your intuition tells you that it "has" to be write-only. Full disclosure: I became interested in this language because I thought there's no way it could be viable - forget about "write-only" and "readability", I was already over those fallacies at that time. But on the other hand, there was contrary evidence [1]. So I gave a honest and earnest shot at it.
What I learned, among other things, is that most of the work is about going against the habits and ways of thinking that you learned from mainstream languages. Of course it is not something one can learn by stagnating in the comfort zone of Algol++ languages.
[1] https://www.forth.com/resources/forth-apps/
the thinking relies on the "sufficiently smart XYZ". similar to - true C++ developers do not write bugs - true agile practitioners build paradise on earth (i jest) - good enough AI coder are vastly more efficient than manual coders etc.
when somebody's first answer to a problem/inquiry is "you're not good enough or else you would see the lights" it raises alarms in my head.
that said, my first steps in programming were forth (and hp RPL). so i got some familiarity, read code, wrote code, wrote basic interpreters too (for learning purpose). forth is cool and has its uses.
This is mostly a problem of concatenative language design. There is nothing preventing function definitions from having type signatures that are statically checked. In my concatenative language compilers, I always provide a function that causes the compiler to halt and print out all of they types on the stack at the call site during compilation as well as a runtime function that prints out the runtime values. I have found this to be quite effective for debugging. Because I don't support global variables, these functions give a complete view of ALL of the live values at any point in the program, which is quite hard in most languages.
Local variables are also not necessary to have reasonable ergonomics, even in mathy contexts. Instead, the language can define special operators that access fields on the stack by type name and move or copy them to the top of the stack. For some reason, in the math example, the author deliberately added an unnecessary parameter that had to be dropped and put the arguments in the wrong order. Cleaning up the signature, there are several reasonable ways to solve the problem in a concatenative language with the features I've described.
where \FOO moves a value of type FOO to the top of the stack and \\FOO copies the value to the top, leaving the original where it was. Yes, the \\ is a little ugly, but these should generally be used sparingly.Even if we restore the original signature and use the author's syntax, there is a much cleaner solution to the problem without the move/copy operators.
This is extremely cryptic. I suspect most forth programmers would have added a stack signature comment, but it is even better if the compiler statically verifies the signature for us, which is how the language I am describing differs from vanilla forth (where stack comments also have the risk of being wrong in an evolving system). If we restore the type system, it becomes: Of course it is still not as obvious to the reader what op_math_full actually does as (y * y) + (x * x) - abs(y). But in practice it would have some name like projection_size so once it has been written correctly, it doesn't really matter that it is a bit obscure to read at a glance. You also get really good at simulating the stack in your head and they type signature makes this much easier. Still, when you are confused, you can always add stack debugging like so: and the output would be something like: Writing programs in this style is very nice so long as you have enough tooling to facilitate it.I have only used concatenative languages that I have implemented myself, however. I have read quite a bit about forth, but for me, there are clear problems with vanilla forth that are solved for me with the meta operators like [, [[, \ and \\ as well as good debug tools like PRINT_STACK above. Forth was an incredible discovery for its time. We can do a lot better now.
Hi, as someone also fiddling around with a concatenative toy language, I wanted to ask if any of your languages have a public repository somewhere? You seem very knowledgeable on the topic and your descriptions made me interested.
Related. Others?
Why concatenative programming matters (2012) - https://news.ycombinator.com/item?id=32124621 - July 2022 (55 comments)
Why Concatenative Programming Matters (2012) - https://news.ycombinator.com/item?id=25244260 - Nov 2020 (18 comments)
Why Concatenative Programming Matters (2012) - https://news.ycombinator.com/item?id=19665888 - April 2019 (33 comments)
Why Concatenative Programming Matters (2012) - https://news.ycombinator.com/item?id=5542695 - April 2013 (36 comments)
Why Concatenative Programming Matters - https://news.ycombinator.com/item?id=3582261 - Feb 2012 (40 comments)
Om is a novel, maximally-simple concatenative language - https://news.ycombinator.com/item?id=33382397 - Oct 2022 (49 comments)
Stem, an interpreted concatenative programing language - https://news.ycombinator.com/item?id=39151094 - Jan 2024 (74 comments)
A Simply Arrived Concatenative Language - https://news.ycombinator.com/item?id=15576215 - Oct 2017 (21 comments)
Factor: A Practical Stack Language - https://news.ycombinator.com/item?id=32215048 - Jul 2022 (45 comments)
Xs: a concatenative array language inspired by kdb+ and FORTH - https://news.ycombinator.com/item?id=23437003 - Jun 2020 (27 comments)
Cat: a statically typed concatenative language - https://news.ycombinator.com/item?id=24534755 - Sep 2020 (13 comments)
8th: a secure, cross-platform, concatenative programming language - https://news.ycombinator.com/item?id=15672361 - Nov 2017 (59 comments)
Programming in the Point-Free Style - https://news.ycombinator.com/item?id=14077863 - Apr 2017 (101 comments)
The author also wrote the "kitten" programming language which also has been talked about on here as well back in 2017:
https://news.ycombinator.com/item?id=13345832
Do most people tend to conclude that concatenative languages are absolutely an intellectual curiosity for any nerd who knows RPN and thought "what if we kept going?", but kind of fail in larger-scale production contexts?
(Yes, I'm aware of PostScript... Seems to be one of the sole exceptions here... And, uh, PDF... of course...)
The odd thing is that RPN is actually pretty intuitive for people. It's the "language" of cash registers! I had never used cash registers, and I lent my HP rpn calculator to my girlfriend to use. I said it's a bit weird, do you need help.
She gave me the dirtiest "are you mansplaining me" look.
Using the HP was perfectly natural for her as she had used cash registers.
I think the fundamental flaw is:
> It is considered good functional programming style to write functions in point-free form, omitting the unnecessary mention of variables (points) on which the function operates.
This is wrong.
It may be considered good programming style in certain small use-cases when a small local composition is clearer. But as a general software engineering principle, the exact opposite is true: It is considered good style to break larger constructs into smaller pieces and give them meaningful names.
There's no reason that function parameters should be an exception to that and in practice, names for function parameters are often some of the most critical names for understanding a system.
I think concatenative languages are really cool as a mental exercise in getting a lot of power out of something simple. But as far as why they aren't used in practice. I think it's largely because they are a bad idea carried to its logical extreme.
I’m pretty sure the programmability of PostScript was a mistake. Adobe clearly thought that because most of it was removed from PDF.
I was working in sales/support/marketing for printer companies in the eighties and people were buying PostScript printers for the scalable fonts, device independence and compatibility. The most common objection was performance which was somewhat related to the programmability.
We used to supply the red and blue books with each printer but I don’t remember anybody breaking the seal on them.
Having said all that I have spent the last year writing a PostScript interpreter.
I’m not sure I’d call PDF concatenative, you certainly can’t cat two PDF files together to make a merged document. There are header and footer sections to deal with.
> I’m pretty sure the programmability of PostScript was a mistake. Adobe clearly thought that because most of it was removed from PDF.
I do not agree. I think the mistake was using PostScript as the output document format, not its programmability. PostScript should be the input format and not the output format.
I think PDF has many problems and is badly designed in many ways, though.
PostScript is a bit like .Net CLR or WASM: while widely used, it's basically never written by humans directly.
Forth is a wonderful way to build a high-ish level self-contained programming environment on very low-resource hardware, like a 8-bit MCU. But thinking in terns of the stack is useless mental gymnastics, this is something a machine should do instead.
People do write in PostScript directly (and I have seen .NET and WASM code written directly, too). I use PostScript directly and so do some other people.
Absolutely. Years ago writing programs for *BSD/Linux, PS was the natural, most direct way to implement printing to printers equipped with a PS interpreter.
Fortunately the PS language was very well documented. That made writing PS pretty straightforward, at least for the reasons I was using it. Curiously other concatenative languages have been harder for me to grasp. Maybe that's because I regarded PS as a specific-use tool vs. a general purpose language.
If nothing else PS showed the value of excellent documentation. Lack of it probably accounts for many software project failures, particularly in the open-source world.
You can also run PostScript programs on the computer; you do not need a PostScript printer.
> Maybe that's because I regarded PS as a specific-use tool vs. a general purpose language.
In my opinion, it is both. Many of the programs I write in PostScript do not involve a printer at all.
> If nothing else PS showed the value of excellent documentation. Lack of it probably accounts for many software project failures, particularly in the open-source world.
I also find a problem with many programs that do not have good documentation. When I write my own, I try to provide documentation.
From what I can tell the mental gymnastics are leaning how to think in terms of Forth instead of how to think in terms of the stack, which is the stack effect and not the stack itself. You only think about the stack itself with your base words (when stack and stack effect are essentially the same), after that it is the stack effect; a single word may push and pop thousands of times but you don't need to think about that, just make sure that it leaves the stack (or stacks) in the state you want it (them). Once you have built up a few layers from the base words I find it more like having a single variable which you store the current state in than a stack, so functional stateless programming but with a tiny dash of state?
There are mental gymnastics but they are far from useless and not just thinking in terms of the stack. I don't quite have it down yet but I am getting closer and can see the sense in it, I haven't quite figured out how to plan and structure a program in Forth.
All programming is just function composition.
This is an idea that I got from studying category theory.
Concatenative programming is just a first step in that direction.
I leave the details an an exercise for the reader.
side effects are quite the thorn in this theories side. hard to describe message passing in purely applicative terms.
Effects are pretty smoothly described by monads; there's an entire well-respected language built on top of that.
One of the problems of the purely applicative approach is that you get a nice algebra for describing your results, including the sequence of effects, but reasoning about the resources spent by the computation becomes harder.
monads were certainly a clever fix for the difficulties otherwise caused by passing around the world as a value.
I might argue that laziness makes haskell something different than purely applicative.
It's not just pure values being passed around. It's a language built atop suspended self-caching invocable thunks getting knotted up and then firing off in chain reaction.
Modern programming is. But look at assembly, which is what actual machines do. And you can write assembly with tons of gotos and not a single function in sight.
Time to read "Debunking the 'Expensive Procedure Call' Myth, or, Procedure Call Implementations Considered Harmful, or, Lambda: The Ultimate GOTO":
https://dspace.mit.edu/handle/1721.1/5753
The point is there's a closer connection between functions and gotos than you might imagine.
There's really no such thing as a 'function' at the assembler level. There are pure jumps and there are jumps that also perform side effects on a register conventionally called a 'stack pointer' (and on the memory that register points to) but no true 'functions.'
This is roughly the point Steele is making, in addition to his point that the stack pointer side effects are often unnecessary (i.e. tail jumps).
Implementing a stack pointer in hardware is merely a legacy hack: Before people figured out continuations they didn't know any better.
If you look at the computer (registers, cache, RAM, storage, etc) as one whole, every assembler instruction is just a function that takes in the state of the computer and returns a new state. Multiple assembler instructions is then using function composition.
> There's really no such thing as a 'function' at the assembler level.
That's like saying "There's really no such thing as a 'chair' at the quantum level."
Modern machines do. But the dominance of imperative assembly is more about historical momentum and engineering tradeoffs than fundamental necessity. We don't have to go all-in on Von Neumann.