I’m tired of these angry rants about how other languages get everything wrong.
I only got as far as the scornful assertion that Python and other languages are still goto-based. They aren’t. Return, break, continue are structured gotos just as much as while and if. (Believe me, I spent my first five years programming using real GOTO statements.)
The lack of a return statement (or exceptions) became the thing I hated the most about Pascal, which was my main language from about 1983-1989. It can makes error handling very awkward and convoluted, since a possible error in the middle of a function often requires refactoring the function to create an exit path. In many cases the nesting got deeper and deeper. There’s a good reason you hardly ever see languages like that anymore.
I’m tired of these angry rants about how other languages get everything wrong.
But it’s lots of fun! People have been doing this with other topics for
millennia.
I only got as far as the scornful assertion that Python and other
languages are still goto-based. They aren’t. Return, break, continue
are structured gotos
Bertrand Meyer is quite full of himself. So am I, but I’m willing to
admit it.
I’d also argue that: “Dogmatically Reading Dijkstra’s Goto Considered
Harmful Considered Harmful”. It took me years to realize that judicious
use of goto is often the best way to deal with error conditions in C.
So much of this I am 100% on board with, but I’m yet to see an argument against break/continue and multiple return points that even claims benefits worth the mangling of code that would otherwise use them.
Most of the argument against multiple returns came from Pascal and related languages (I can’t remember if Algol did this). In Pascal, the name of the function was exposed in the function as a variable that you could assign to and that would then you had a separate return statement that did the control-flow transfer. Because setting the return value and returning were different steps, if you had multiple return paths then it was easy to return the wrong thing (forgetting to set the return value on that path).
There were also reasons to avoid it in languages without lexically scoped destructors, because it made it easy to forget to release locks and so on. If generally prefer to use GCC’s attribute cleanup if I have to write C (which also makes it possible to write exception-safe C), so multiple return paths don’t leak locks or other resources. In any language with RIAA, this is not a problem.
Compilers from the ‘80s or earlier also did not do a good job of code generation for functions with multiple return paths because they often avoided keeping much state while generating code.
Oh, and if you ever want to prove properties about your code then reducible control flow can make it slightly easier (though not much, reasoning about arbitrary intraprocedural control flow graphs is fairly well understood now).
Those are historical precedents, not actual benefits of the restriction. If you’re setting a result value, now instead of looking for multiple return statements you’re scanning the combinatorial explosion of all possible paths within the function looking for places where the result might have been set. Doesn’t seem at all better to me. And as for “not setting the return value at all” that’s only an argument in the no-longer-common result var languages, it’s impossible to do so if you have to return a value when you return.
Looking at the Eiffel’s code examples, my gut feeling is that a lot of early-returns get folded into preconditions. And early returns are by far the most frequent case for these control flow operations.
That’s interesting, and blessedly short :) But not a very strong argument for the general case IMO, it basically boils down to the standard “it’s basically a goto therefore bad, QED”. But these constructs are not GOTO, and to claim they are equivalent requires overlooking all the aspects of traditional GOTO that were the most important reasons it’s considered harmful.
Many expression-based languages have a nice property, that you can rewrite
def f(args) = ... exp ...
into
def f(args) = ... g(vs) ...
def g(vs) = exp
where vs are the free variables of the expression exp. This property is very useful for refactoring and reasoning about code in general. With early return this nice property no longer holds. You can’t rewrite:
def f(x) = if (x > 0) { y = x + 1; return y }; return -x
into
def f(x) = if (x > 0) { g(x) }; return x
def g(x) = y = x + 1; return y
It’s true that that’s a nice feature, but early return is very useful too. I’ve been writing a fair bit of elixir recently and I often find myself wanting to do an early return. I can rephrase things, but it’s sometimes quite difficult to come up with a nice alternative.
It is quite amusing that section B.1 sets up the category of finite sets FinSet. I suppose that the author thought that identity functions were too obvious to introduce. This section has the only use of “composition”. This makes section 7.6, the only use of “monad”, much funnier.
The introduction to chapter 10 contains misconceptions. It states:
One should note in passing that functional programming does not solve the problem. Functional languages do address it in part by removing the notion of pointer or reference and providing instead built-in classes for common data structures such as lists, hiding pointer manipulations from the programmer. But any modeling of complex data structures not covered by the library (general graphs) would require the introduction of a pointer-like mechanism.
However, directed graphs can be modeled using the same functions introduced in B.1. Let E be the type of edges and V be the type of vertices; then, a graph is given by two functions from E to V which assign each edge its source and target vertices. The author’s point may be that E and V are infinite (say, the natural numbers) but a given graph is finite; in that case, use total functions from E to V + 1 to model partial functions, as usual. No pointers required.
You can’t have it both ways. If you do not want overloading, then do as ML and introduce a different addition operator for floats rather than overloading the integer +.
He doesn’t say this. He distinguishes between “syntactic overloading” and “semantic overloading”. He declares syntactic overloading harmful, while semantic overloading is beneficial.
In semantic overloading, there is a named function with a well defined signature and contract, and it has multiple implementations, based on the argument types. Overloading the arithmetic operators for floats and ints is an example. Haskell type classes are an example of semantic overloading. So are generic functions in Common Lisp. So is method dispatch in a single-dispatch OOP language. Semantic overloading is what makes generic programming possible.
Syntactic overloading has no such justification. Syntactic overloading means that the same name has multiple unrelated meanings in the same lexical scope, and is pretty much as bad as Meyer says it is.
Mini nitpick: as far as I know, Standard ML and F# allow + for both integers and floats; I’m only aware of OCaml that makes the distinction with + and +..
Removing overloading has other knock-on consequences. In an OO language, you can have T and T’ that both implement a doStuff() method, and so you can write generic code that calls x.doStuff() and is valid for the set of types containing at least T and T’. But, without overloading, you cannot write code that calls doSomethingElse(x) for which the same holds. This restricts the power of your generics: they cannot be used to implement multi-dimensional dispatch without explicit type casting. That may be a choice that you want to make but it should be a choice that you make explicitly.
it also has interesting consequences for structural types. If a class provides an implementation of foo(X) and foo(Y) and both are structural types then you have a complex constraint problem to solve to figure out which to call (unless you require explicit casting to X or Y, in which case you may as well just use different selectors). Worse, if a concrete type implements foo(T) and a structural type requires foo(X), where T is a subtype of X, then does this class conform to the structural type?
My conclusion so far with overloading is that having it and not having it in a language are both the wrong decision.
I’m tired of these angry rants about how other languages get everything wrong.
I only got as far as the scornful assertion that Python and other languages are still goto-based. They aren’t. Return, break, continue are structured gotos just as much as while and if. (Believe me, I spent my first five years programming using real GOTO statements.)
The lack of a return statement (or exceptions) became the thing I hated the most about Pascal, which was my main language from about 1983-1989. It can makes error handling very awkward and convoluted, since a possible error in the middle of a function often requires refactoring the function to create an exit path. In many cases the nesting got deeper and deeper. There’s a good reason you hardly ever see languages like that anymore.
But it’s lots of fun! People have been doing this with other topics for millennia.
Bertrand Meyer is quite full of himself. So am I, but I’m willing to admit it.
I’d also argue that: “Dogmatically Reading Dijkstra’s Goto Considered Harmful Considered Harmful”. It took me years to realize that judicious use of goto is often the best way to deal with error conditions in C.
I’m far more tired of using programming languages that do things wrong, than I am about people pointing out what those things are.
So much of this I am 100% on board with, but I’m yet to see an argument against break/continue and multiple return points that even claims benefits worth the mangling of code that would otherwise use them.
Most of the argument against multiple returns came from Pascal and related languages (I can’t remember if Algol did this). In Pascal, the name of the function was exposed in the function as a variable that you could assign to and that would then you had a separate return statement that did the control-flow transfer. Because setting the return value and returning were different steps, if you had multiple return paths then it was easy to return the wrong thing (forgetting to set the return value on that path).
There were also reasons to avoid it in languages without lexically scoped destructors, because it made it easy to forget to release locks and so on. If generally prefer to use GCC’s attribute cleanup if I have to write C (which also makes it possible to write exception-safe C), so multiple return paths don’t leak locks or other resources. In any language with RIAA, this is not a problem.
Compilers from the ‘80s or earlier also did not do a good job of code generation for functions with multiple return paths because they often avoided keeping much state while generating code.
Oh, and if you ever want to prove properties about your code then reducible control flow can make it slightly easier (though not much, reasoning about arbitrary intraprocedural control flow graphs is fairly well understood now).
But break/return/continue don’t make control flow irreducible, no? You need full goto to create it?
Those are historical precedents, not actual benefits of the restriction. If you’re setting a result value, now instead of looking for multiple return statements you’re scanning the combinatorial explosion of all possible paths within the function looking for places where the result might have been set. Doesn’t seem at all better to me. And as for “not setting the return value at all” that’s only an argument in the no-longer-common result var languages, it’s impossible to do so if you have to return a value when you return.
The most nuanced take on this I’ve seen is https://www.teamten.com/lawrence/programming/avoid-continue.html.
Looking at the Eiffel’s code examples, my gut feeling is that a lot of early-returns get folded into preconditions. And early returns are by far the most frequent case for these control flow operations.
That’s interesting, and blessedly short :) But not a very strong argument for the general case IMO, it basically boils down to the standard “it’s basically a goto therefore bad, QED”. But these constructs are not GOTO, and to claim they are equivalent requires overlooking all the aspects of traditional GOTO that were the most important reasons it’s considered harmful.
Many expression-based languages have a nice property, that you can rewrite
into
where
vs
are the free variables of the expressionexp
. This property is very useful for refactoring and reasoning about code in general. With earlyreturn
this nice property no longer holds. You can’t rewrite:into
It’s true that that’s a nice feature, but early return is very useful too. I’ve been writing a fair bit of elixir recently and I often find myself wanting to do an early return. I can rephrase things, but it’s sometimes quite difficult to come up with a nice alternative.
It is quite amusing that section B.1 sets up the category of finite sets FinSet. I suppose that the author thought that identity functions were too obvious to introduce. This section has the only use of “composition”. This makes section 7.6, the only use of “monad”, much funnier.
The introduction to chapter 10 contains misconceptions. It states:
However, directed graphs can be modeled using the same functions introduced in B.1. Let E be the type of edges and V be the type of vertices; then, a graph is given by two functions from E to V which assign each edge its source and target vertices. The author’s point may be that E and V are infinite (say, the natural numbers) but a given graph is finite; in that case, use total functions from E to V + 1 to model partial functions, as usual. No pointers required.
Declares method overloading considered harmful
Goes on to overload mathematical operators
You can’t have it both ways. If you do not want overloading, then do as ML and introduce a different addition operator for floats rather than overloading the integer
+
.He doesn’t say this. He distinguishes between “syntactic overloading” and “semantic overloading”. He declares syntactic overloading harmful, while semantic overloading is beneficial.
In semantic overloading, there is a named function with a well defined signature and contract, and it has multiple implementations, based on the argument types. Overloading the arithmetic operators for floats and ints is an example. Haskell type classes are an example of semantic overloading. So are generic functions in Common Lisp. So is method dispatch in a single-dispatch OOP language. Semantic overloading is what makes generic programming possible.
Syntactic overloading has no such justification. Syntactic overloading means that the same name has multiple unrelated meanings in the same lexical scope, and is pretty much as bad as Meyer says it is.
Mini nitpick: as far as I know, Standard ML and F# allow
+
for both integers and floats; I’m only aware of OCaml that makes the distinction with+
and+.
.Removing overloading has other knock-on consequences. In an OO language, you can have T and T’ that both implement a
doStuff()
method, and so you can write generic code that callsx.doStuff()
and is valid for the set of types containing at least T and T’. But, without overloading, you cannot write code that callsdoSomethingElse(x)
for which the same holds. This restricts the power of your generics: they cannot be used to implement multi-dimensional dispatch without explicit type casting. That may be a choice that you want to make but it should be a choice that you make explicitly.it also has interesting consequences for structural types. If a class provides an implementation of
foo(X)
andfoo(Y)
and both are structural types then you have a complex constraint problem to solve to figure out which to call (unless you require explicit casting toX
orY
, in which case you may as well just use different selectors). Worse, if a concrete type implementsfoo(T)
and a structural type requiresfoo(X)
, where T is a subtype of X, then does this class conform to the structural type?My conclusion so far with overloading is that having it and not having it in a language are both the wrong decision.