Is that really a debate? Gosh, I'd love to see the "separatist" arguments, because ever since I've discovered Lisp, I've automatically assumed that separating statements and expressions is one of those stupid historical baggage things, a stepping stone in early PL design that we can't just get rid of.
I mean, how many libraries with weird APIs were created just because you can't write the following in most Algol-like languages:
In many languages, people are resorting to "immediately invoked function expressions" to simulate this pattern (whether for returning or assigning). And those that can't, well, here goes another pointless little function to encapsulate it[0].
--
[0] - Which transitions into what is a real debate - "lots of small functions" vs. "fewer but larger functions". It's one of those holy wars that can't die, because both sides have good arguments. But it's a false choice - a limitation of the tools we're using to write programs. 'emilprogviz has a nice summary of that last point here: https://emilprogviz.com/ep05/ep05-transcript.html
Linking to transcript, in the spirit of "text with screenshots is almost always better than a video" - (nice job providing it Emil!) - but the video itself is good too, as are others on that site.
Ok it may not exist as a debate outside my own head :)
To offer one argument: the problem with expression oriented languages is their generality, in that you can write "weird" expressions, e.g.:
let x =
[giant block of code with multiple nested lets, ifs, etc.]
in
f(x)
I've definitely done this a number of times. Languages that separate statements and expressions force you to break things down further and prevent the code from going too far to the right.
Also, for low level languages, the mapping between code and assembly is clearer in languages that separate statements and expressions (I need a more succinct term).
EDIT: Also, by preventing statements from being used as expressions, you encourage breaking up long and complex statements into smaller functions.
Oh, I see what you mean. I discovered Lisp after working in C++ and Java, so I avoided this pitfall in my code, but if I had a dollar for every instance of:
(let ((some-variable (progn
(do stuff)
(do other stuff)
(let ((some-helper-var ...))
(some more code with some-helper-var)
(more-of-the-same (progn
...)))
;; 50-100 lines later, just return one of the values
some-variable)
that I saw in Lisp codebases, particularly in Emacs, well... I could feed my family for a month or two from that money.
There's plenty of abuse potential here (and for the love of god, if your language has 'let', it probably also has lambdas or 'flet', use that to create local functions...). But mitigating this, arguably, is a problem for style guides - the overall feature of "everything is an expression" is powerful. It has nice simplicity to it, and reduces boilerplate :).
> Also, for low level languages, the mapping between code and assembly is clearer in languages that separate statements and expressions
I'm guessing this is where the separation originally came from. Assembly is essentially statement-only. But at this point, I think all programming languages in use crossed the threshold where we're actually programming to an "abstract machine", and being expression-oriented seems to confer greater expressiveness.
> (I need a more succinct term).
"Separatist"? :).
> EDIT: Also, by preventing statements from being used as expressions, you encourage breaking up long and complex statements into smaller functions.
Ah yes. The real debate. I edited my comment to mention it before I saw your edit :).
Yes, thank you, your code example is much better than mine.
The simplifying potential of expression-oriented languages is huge. Alan Perlis said[0]: "symmetry is a complexity-reducing concept; seek it everywhere", and this is a great example of that.
For example, languages like C and Ada have both if statements and if expressions, this is a duplication that can be eliminated by making them expression-oriented so you only have an if expression like in Haskell or ML.
But, interestingly, there is one historical case of a language going from expression-oriented to statement/expression separation: ALGOL-W[1] was expression oriented, Pascal[2], its successor, separates statements from expressions. Wirth designed both.
I don't know what the motivation was, but I suspect it's because Pascal was designed to be an educational language, and Wirth must have thought that separating expressions and statements made didactic sense when teaching programming as a recipe or list of things to do, as opposed to the more mathematized formulation of expression-oriented languages (of having an evaluation function from expressions to values).
The successors of Pascal (Ada, Modula and its sequels) retain the statement/expression separation.
I can't say for sure, but I suspect it was a reaction to ALGOL-68. Creators of later Algolish languages usually made a point of rejecting features of Algol-68 they saw as prone to abuse.
And the block expressions in Algol-68 were certainly used and abused. Basically, the type and value of a block (BEGIN ... END or ( ... )) are those of its last expression, so you can put them anywhere. For example, Algol-68 has the looping construct WHILE <condition> DO <body> OD, but not C's `do <body> while (<condition>)`. So what do you do if you want to have the test at the end of the loop body? Simply
WHILE (
<body>;
<condition>
)
DO SKIP OD
I guess you'd get used to those idioms eventually, or some coding conventions would have arisen if the language had been successful. But I can understand language creators looking at that and seeing how getting rid of it simplifies not only their compilers but also the programs written in their languages.
That introduces a variable that has to be initialised outside the control block. It's not terrible as hacks go, but I guess the readability cost of the additional variable is higher than having the body in the condition.
> Oh, I see what you mean. I discovered Lisp after working in C++ and Java, so I avoided this pitfall in my code, but if I had a dollar for every instance of: [...]
This is one of those anti-patterns I sometimes find myself falling into (I don't code Lisp or its descendants often). It's kind of discouraging, to be honest, because it feels like there ought to be some more efficient way to do this, and my inner critic comes along and complains that I'd be better of writing Python or C than learning to do it the Right Way in Lisp.
Is there some kind of idiomatic way to avoid this and write cleaner code? Is the solution to simply extract the progn into another function? Does that violate some rules of function encapsulation in Lisp?
I think extracting functions is the way to go. I like it when a function's body reads like a sentence, when it's been broken down to the atoms of that conceptual level of abstraction. Forth code that achieves this can look very satisfying.
The problem is that the toplevel of a module is full of functions at various levels of granularity.
It might be nice if programming languages had a concept of "code sections" (you could implement this with literate programming), where modules are organized hierarchically into sections, and declarations can be public or private within a section. So you might have:
section foo
// Accessible from outside this section
public important_function()
// Only accessible from inside this section
private utility_function_1()
private utility_function_2()
end section
> The problem is that the toplevel of a module is full of functions at various levels of granularity.
Exactly--this is what I was hinting at with "rules of function encapsulation." I took a course on LISP in college (apropos of nothing, it featured a lab called, "Isn't this just a one credit course?") in which it seemed like the LISP Way was to use helper functions. It's always felt like something was missing in my ability to "translate" between paradigms because this rubs me the wrong way (although as I went through some examples I realized I do this with some regularity in other languages--but it doesn't "feel" as bad).
In retrospect probably part of the problem was that we ALGOL-adjacent undergrads didn't have the scaffolding to understand more generic approaches like fold and cousins. It was a different way of thinking.
I like the idea of literate programming as a potential solution, especially as it can be used to guide newbies through the code and identify areas where idioms are much different between one's existing paradigms and that of the codebase.
In Common Lisp you can use FLET or LABELS to declare local functions. This works, but there really is no good solution: utility functions often just take up space and belong nowhere.
The issue with these debates is that these tend to be driven by people with strong opinions coming usually from misunderstanding of the other side of the debate.
I like to think that there is no OOP vs FP, there is no strong typing vs no typing, there should be just adding to a shelf with tools that each has application in some situations but also constraints on when it can be used effectively. And your job as developer is to understand the bounds on application and effectiveness.
The road to mastery of development then should be by learning and understand those various tools (in the broadest possible sense) rather than by forming strong opinions and shunning the other side of the debate. People who cut off themselves from OOP will never learn its benefits just as people who cut themselves from FP.
Well, I'm not sure I'm on one side or the other of the statements-or-no-statements debate, but in general I think the argument in favor of separating statements from expressions is that it adds redundancy to your programs, which makes them easier to read and enables the compiler to produce better error messages when you have a syntax error. Sometimes you can trade this off against other aspects of syntax you'd like to improve; Lua, for example, omits semicolons and isn't white-space sensitive, but the compiler can still emit reasonable syntax error messages because of its strict separation of statements and expressions.
You can transform this to the following in even the most limited ALGOL-like languages, which is less clear but not nearly as heinous as the alternatives you mention:
The more popular ALGOL-derived programming languages like C, Java, and post-walrus Python have conditional expressions and assignments inside expressions, which means that in this case you don't need to resort to declaring a variable. In Golang and Pascal you can just assign to the named return value rather than declaring it as a normal variable and then explicitly returning it.
The argument against separating expressions from statements is also that it adds redundancy to your programs, as in the above example.
I think the "separatist" side (yep I'm adopting your term) has already accepted that the war is lost, but I still see battles being waged often in programming language spec committees, whenever there's a suggestion for allowing if to be treated as an expression.
The arguments against it are always that it requires having keywords that behave differently when being expressions or statements.
Often the alternatives and compromises proposed always have the same issues, such as Javascript repurposing the do keyword for enclosing expression ifs.
The intuition that I've built up is that most of the time that you have a debate that goes on for more than about 5 minutes, there's a problem at a different abstraction level.
For instance with Imperial vs Metric System, Metric optimizes for multiplication, while Imperial optimizes for division. You can never unify these under the current system. But you can, if you change our base to base 12. Then they suddenly merge.
With CLI vs GUI, we've realized that we needed a mixture. We need a GUI that runs through a CLI. And now we have that, it's called a website. I think tabs vs spaces was solved similarly, with tabs as spaces that editors config can treat n-straight-spaces as tabs.
I'm firmly on the expression only and lots of functions sides, but that transcript is very interesting. You can already do local functions in C#, it seems to be implying that we should strive for that to be our mixture.
> The intuition that I've built up is that most of the time that you have a debate that goes on for more than about 5 minutes, there's a problem at a different abstraction level.
I strongly agree. Also, that's poetically put. I'm saving this in my quotes file.
I mean, how many libraries with weird APIs were created just because you can't write the following in most Algol-like languages:
In many languages, people are resorting to "immediately invoked function expressions" to simulate this pattern (whether for returning or assigning). And those that can't, well, here goes another pointless little function to encapsulate it[0].--
[0] - Which transitions into what is a real debate - "lots of small functions" vs. "fewer but larger functions". It's one of those holy wars that can't die, because both sides have good arguments. But it's a false choice - a limitation of the tools we're using to write programs. 'emilprogviz has a nice summary of that last point here: https://emilprogviz.com/ep05/ep05-transcript.html
Linking to transcript, in the spirit of "text with screenshots is almost always better than a video" - (nice job providing it Emil!) - but the video itself is good too, as are others on that site.