1. 19
    1. 19

      It’s interesting that ‘implicit returns’ are being presented as something novel, while in actual PLT history, implicit returns came over a decade before the ‘explicit’ return statement.

      Lisp (1958) is the first language I know with implicit returns. 1958 is also the year (AFAIK) that user defined functions first appeared in programming languages. Fortran II introduced this feature the same year, but the way you returned a value from a function was by assigning a value to the function name before executing a RETURN statement. Algol 68 had only implicit returns, no RETURN statement. In BCPL, the ancestor of C, the body of a function was an expression, not a statement list, but you could use a valof expression containing imperative statements as a function body.

      AFAIK, C, in 1972, invented the ‘explicit’ return statement, where you write ‘return ’ to return a value from a function. The BCPL syntax for returning a value from a function was not used because the algorithm for parsing and generating code for the BCPL function syntax would have required keeping the parse tree for an entire function in memory (according to a story attributed to Dennis Richie). And the machine they were using did not have enough memory for that. Once C became popular, it seems that every subsequent language copied the C explicit return syntax, and the older ‘implicit return’ syntax was kept alive by functional languages descended from Lisp and Algol 68. The repopularization of functional programming during the 21st century has brought back the ‘implicit return’ syntax.

      1. 3

        BCPL had a return command to return from a procedure without a value, and a resultis command to return the value from a valof block. https://www.bell-labs.com/usr/dmr/www/bcpl.html

        C probably got its return from PL/I — see bottom right of p.11 of https://dl.acm.org/doi/10.1145/363707.363708 (1965) — along with its comment syntax, auto, static, and maybe some other bits and bobs. PL/I was the primary implementation language of Multics, so ken and dmr knew it well.

        1. 3

          Cool, thanks for the NPL link. They are borrowing ideas from Lisp and APL, and this is now my earliest sighting of the ‘if … then … else …’ conditional expression using that syntax (rather than McCarthy’s notation from the 1960 LISP paper).

      2. 10

        In general, readability is about expectation, which is cultural and subjective. There is no objective truth here, so examples without a persuasive argument are likely to fall flat. The frame of this article seems to assume otherwise, which I suspect will reinforce the prior opinions of each reader.

        The focus on readability misses some of the most valuable points in favor of expression evaluation. Notably the way they compose with other expressions and operators. Rather than pretending the example languages are providing equivalent constructs, I suspect an exploration of the differences between each of these languages operators would have revealed more to the reader. e.g. Maybe delve into how the return keyword in ruby has different rules than the other example languages scoping.

        1. 16

          I’ve read multiple articles about coding style that began with the author presenting two alternate implementations of the code where I agreed with the author that one was obviously unreadable, only to be increasingly confused as the article continued because I has chosen the “wrong” example when deciding which one was unreadable.

          I specifically remember one write up where the author presented “unreadable” code using early returns and a “clearer” version that required if statements nested six layers deep (the “unreadable” version had no nested conditionals and was half the length). I was so surprised when I realised that the article was arguing against early returns that I assumed that it was supposed to be a satire. However, after reading some of the author’s code, I realised that he was completely serious and found conditionals nested twenty layers deep perfectly readable. I remember him doing another another article where he argued that the only appropriate tab width was one, because too much code falls off the right side of the screen at higher widths.

          1. 13

            I remember him doing another another article where he argued that the only appropriate tab width was one, because too much code falls off the right side of the screen at higher widths.

            They were this close to understanding the problem.

            1. 2

              In the strictest formulations of structured programming, subroutines are to have exactly one entrance and one exit, and there are no early exits from any other structures (for example, there’s no break or continue statements in loops).

              Oberon, at least in its later versions, enforces this by allowing a RETURN statement only in the last position of a subroutine body and by removing break/continue.

              (Also, a common style of coding on the Amiga is/was to have very deeply nested ifs: every initialization statement was wrapped in an if and nested inside the previous initialization statement. That’s how I first started writing reasonably large C programs and it took me a while to break the habit.)

              1. 2

                In the strictest formulations of structured programming, subroutines are to have exactly one entrance and one exit, and there are no early exits from any other structures (for example, there’s no break or continue statements in loops).

                Doesn’t that only refer to there only being a single place you can return to? Returning to different places depending on circumstance obviously violates structured programming, but having multiple return statements doesn’t make your code less structured.

                1. 1

                  Doesn’t that only refer to there only being a single place you can return to?

                  I doubt that there’s any way to decide what structured programming requires. There are lots of formulations of what structured programming; the concept is a moving target. (Not as bad as REST, but a similar problem.)

                  That said, lots of people certainly believe that structured programing, as they understand that term, rules out more than one exit from a function. E.g., here and here.

            2. 5

              Thanks for reading!

              In many cases, the readability is subjective, but readability as a whole can’t be totally 100% subjective, can it? There must be cases where everyone would agree X is more readable than Y. We just don’t talk about those.

              Anyhow, I wanted to talk about the subjective via just hitting enough examples that it might start to give people with a different perspective a glimpse into what I’m seeing.

              1. 1

                There are some objective comparisons of languages that overlap with readability. I’d put them more into the information theory / efficiency category though e.g. Signal to noise ratio, where redundant symbols carry no new information or we have to record information for the machine rather than the reader. However we’ve been past that level of language design since about 1980. We aren’t manually specifying line numbers ala BASIC and we aren’t (generally) re-specifying the same register or memory offset for the same value as in assembly.

                Contrast that with brainfuck. Completely unreadable for probably everyone here, particularly on a relative basis to any other language. However, the reason it is unreadable is mostly due to it’s completely unfamiliar vocabulary and spacial conventions. It would be just as readable as assembly for anyone who grew up with that vocabulary.

            3. 2

              This post started as a conversation: me trying to explain to someone why implicit returns are great. For them, implicit returns were novel and more challenging to read. I don’t think I convinced them. But now I have an entire article, so that may do.

              I like implicit returns, if expressions, and expression-based programming in general. I find it easier to read than an early return style.

              What’s your take? Do implicit returns improve or hinder readability?

              1. 7

                You can’t return statements, that just doesn’t make sense.

                For what it’s worth, in C++, you can return expressions that evaluate to void. This is essential for making a bunch of templated things work (templated function takes a callable and returns the same type as the callable thing).

                I don’t think implicit return is the right way of thinking though. To me, it comes down to expression-oriented or statement-oriented design. Do you have an if statement or an if expression? i.e. is if (x) then y (in your local syntax) going to be something that evaluates to y or something that just does y and is cannot be nested within another expression? C is deeply weird here because it has both statement (if) and expression (ternary ? : ) forms of conditionals, but loops are all statements.

                There is huge value in an expression-oriented design, because it allows cleaner composition. An if expression evaluates to different things on its two paths and that value propagates out up the AST. This separates out the two conflated behaviours in return statements:

                • A flow-control operation that pops the top frame from the stack.
                • A specification of the return value.

                Pascal (which is top of everyone’s mind today) made an interesting decision here. It deconflated these ideas by not implementing the first at all, and using assignment to the name of the function to specify the return value. This was necessary because it was a statement-oriented language. An expression-oriented version of Pascal would have simply made each function a single expression.

                My understanding is that statement-oriented languages primarily existed to allow line-by-line translation. BASIC, for example, executed each statement in the interpreter keeping minimal interpreter state between each. Early C compilers processed one statement at a time and just kept the local symbol table around. Lisp and Smalltalk needed to build a complete AST for each function (though both cheated a lot by making most expressions nested functions). If C had had the same design, you’d have needed to build an AST for each function to be able to compile and that wouldn’t have fitted in the 128 KiB of RAM that the PDP-11 had. Modern C compilers do build a complete AST, but as TCC showed that isn’t actually necessary and you can still compile modern C one statement at a time.

                There is little reason to build a statement-oriented language now.

                1. 3

                  I don’t think implicit return is the right way of thinking though. To me, it comes down to expression-oriented or statement-oriented design. Do you have an if statement or an if expression? i.e. is if (x) then y (in your local syntax) going to be something that evaluates to y or something that just does y and is cannot be nested within another expression? C is deeply weird here because it has both statement (if) and expression (ternary ? : ) forms of conditionals, but loops are all statements.

                  There is huge value in an expression-oriented design, because it allows cleaner composition.

                  There was a great conference talk by Yehuda Katz years back (sadly, hosted on Blip.tv, RIP) where he compared Ruby and Python as languages, and the main takeaway was that in Ruby, everything is an expression.[1]

                  This was an important epiphany for me, and it made the language differences make a lot of sense. You can’t just put any Python code in a lambda, you have to know what is a statement vs what is an expression, whereas in Ruby you don’t have to think about it.

                  This problem is even more evident in React, because you can’t use if, switch etc in combination with angle brackets so you frequently see massive, unreadable ternary expressions.

                  [1] I know this isn’t 100% accurate, but it’s accurate enough.

                  1. 2

                    using assignment to the name of the function to specify the return value

                    This syntax came from Algol 60. Maybe its weirdness is explained by how early it was in the development of programming notation.

                    (But not Algol 58, which separated functions and procedures; a function’s value was a single expression.)

                    There is little reason to build a statement-oriented language now.

                    I was interested by Strachey’s argument in his paper “fundamental concepts in programming languages” for keeping statements and expressions separate in CPL. (Like Algol 58, CPL separates functions and procedures too; I think C inherited its separate conditional expressions and statements indirectly from CPL.) Strachey wanted expressions to be a pure sublanguage, which seems to me like a much more principled reason than any of the languages that came later.

                    Still, I think you are right. Experience has shown that Strachey worried too much about the impenetrability of expressions with side-effects. He wasn’t completely wrong to worry, tho, which is why the issues are being tackled with effect systems and affine types and what have you.

                    1. 4

                      Experience has shown that Strachey worried too much about the impenetrability of expressions with side-effects

                      Sequence points in C have caused a lot of pain and suffering. In an expression-oriented language, it’s important to clearly define ordering. I believe C didn’t do this because it often meant evaluating things in the wrong order for pushing them onto the stack (C ABIs push arguments right to left, so that the first argument is always top, which is necessary for variadics to work). If function arguments are evaluated right-to-left, but expressions are evaluated right-to-left, you have orders like this:

                      int a = 1;
                      foo(a++, ++a); // will be foo(2, 2) and a will be 3 at the end
                      

                      This is deeply confusing to specify and not all C implementations will want to do this kind of craziness (in a memory-constrained compiler, you skip to the last argument, evaluate that and push it, then skip back to the next-last one, evaluate and push it, and so on). Again, in a modern compiler, it’s fine to build an AST, evaluate a++ first, then ++a, then the function call and get a clear evaluation order.

                      Even then, there can be corner cases. If you don’t have strictly left-to-right evaluation, the order of things like ++a++ can be confusing (and possibly even then). Smalltalk enforces a strictly left-to-right order, but that confuses people because they expect a + b * c to be evaluated as a + (b * c) because mathematicians enjoy introducing confusing rules for people to memorise when the alternative is consistency.

                      This is why, in Verona, we permit non-bracketed chains of only the same operator. For us, a + b * c is a syntax error and you must write either a + (b * c) or (a + b) * c, for any pair of operators, but you can write a + b + c (for any operator, including user-defined ones, so if you want to write a butNever b with butNever as an infix operator you can. Whether you should is a different question).

                      1. 3

                        Sequence points in C have caused a lot of pain and suffering.

                        I did say Strachey wasn’t wrong to worry :-) Languages have learned from C’s mistake by making assignments return () or void or whatever they call their unit type. And this would also prevent C++’s mistake of making the result of an assignment expression be an lvalue. (It’s an rvalue in C.) As for pre/post inc/dec, probably best to yeet them into Mount Doom.

                        in Verona, we permit non-bracketed chains of only the same operator

                        That sounds sensible. It reminds me of occam a bit, but occam required expressions to be completely bracketed, which was frankly a pain in the arse.

                        I quite like the idea of chained relational operators like Python, but I think a language should require that they all point in the same direction. This rule is also good style in languages without chained relations, so I write things like min <= i && i < max instead of i >= min && i < max — but even better if the language allows/encourages min <= i < max.

                        1. 1

                          This is why, in Verona, we permit non-bracketed chains of only the same operator. For us, a + b * c is a syntax error and you must write either a + (b * c) or (a + b) * c, for any pair of operators, but you can write a + b + c (for any operator, including user-defined ones, so if you want to write a butNever b with butNever as an infix operator you can. Whether you should is a different question).

                          I like this idea a lot, but it feels like I’d get annoyed with the extra parentheses for logical operations in an if. e.g. if (foo() <= 0 && bar() != 5) needing if ((foo <= 0) && (bar() != 5)). Perhaps it’s something you quickly adjust to though?

                          1. 2

                            I tend to write the latter anyway because it isn’t obvious to me that && has a lower precedence than <=. The converse is confusing without brackets: if (foo() & a != b) is if (foo() & (a != b)), which is never what you actually want.

                            When you read the versions with the parentheses, it’s immediately obvious what is happening, even if you haven’t memorised all 17 rows of the C/C++ operator precedence table (without looking it up, can you tell me what A || B && C does? Would you be surprised that it’s A || (B && C)?).

                            I’ve made this style a requirement in CHERIoT. It takes very little adjustment for the people writing the code and none for the people reading it.

                            1. 3

                              Would you be surprised that it’s A || (B && C)?

                              No, it makes sense to me, but only because in Boolean algebra, B && C can be written as BC or even B * C (because 1 * 1 == 1 and 1*0 == 0 or True * True == True and True * False == False) and A || x is usually written as A + x (because 1+0 == 1 and 1+1 == 1 or True + False == True and True + True == True). If you know your normal precedence rules, this makes sense.

                              What doesn’t make sense is the C precedence table, but there is a reason for it (even if it isn’t a very good reason long term).

                        2. 2

                          using assignment to the name of the function to specify the return value

                          This syntax came from Algol 60.

                          Fortran II in 1958 also had this syntax. I assume that Algol 60 copied from Fortran. I don’t know any languages before 1958 that had user defined functions in a form we would recognize today.

                          The syntax made sense because in the 1950’s, you mostly programmed in assembly language. There was no stack and hence no recursive functions. Each ‘subprogram’ used global variables to communicate arguments and results with the calling program, as well as store the return address. A subprogram that behaved like a function had a global variable containing its return value, probably named after the function, and inside a function, you assigned to this global variable to specify the result before returning.

                          1. 1

                            But not Algol 58, which separated functions and procedures; a function’s value was a single expression.

                            I have a vague memory that PL/SQL or T-SQL has this distinction too.

                          2. 2

                            My understanding is that statement-oriented languages primarily existed to allow line-by-line translation.

                            Interesting! Where would I look to learn more about this?

                            1. 2

                              I am going mostly from papers about FORTRAN implementations from the ‘60s and ‘70s.

                          3. 4

                            I think implicit returns have the benefit of reducing boilerplate. This can be good or bad - when a function has a lot of nested conditions, it can be really hard to figure out where the returns are. Of course, nested conditions are a smell in and of themselves, but we’ve all seen code like that. On the other hand, when a function is written well and the implicit return is a well known pattern, it makes code much more obvious. Implicit returns are actually more manageable in typed languages because the return type is declared in the function syntax and returning a null value or returning the wrong thing is (hopefully) a compiler error.

                            As someone who started out in Ruby, I may be biased, but I like implicit returns. I use the return keyword for early exits or a guard clauses, so my eyes are always drawn to returns as exceptional cases outside the happy path. On the other hand, ending a function with an expression has the same feeling as the period at the end of a sentence.

                            1. 2

                              Yes, totally, once you adopt implicit returns, then it improves understanding when you hit an early return.

                              Also, agreed, works better with return types.

                            2. 1

                              Do implicit returns improve or hinder readability?

                              Hinder. But that’s because I’m used to the return keyword being present, and every time I read ML or whatever where there’s a line that says:

                              0
                              

                              I segfault and reboot and take an extra 250ms to comprehend the line. It’s like running a marathon with someone riding on a scooter alongside me and hitting me on the shins with a 2x4. So not great.

                              But if I grew up with Lisp or ML or whatever, I’d hate that people have a return keyword in their language. I mean, seriously, a keyword?!?

                              So implicit returns are objectively worse, for me. (Objective, in the sense that I can measure the difference.) But they aren’t objectively worse in the general population sense.

                            3. 1

                              I would love an IDE extension that auto annotated the returns so you could toggle it on and off. When I’m first learning a lang it’s confusing, but once you’re fluent then it’s easier to have them omitted. An IDE option might let us have our implicitness and eat it too.

                              1. 1

                                BLISS from 1970 also has this:

                                function factorial(n) =
                                if .n <= 1 then 1 else .n * factorial (.n - 1);