Looks good. The control flow of "handle" looks a bit confusing, but overall I definitely like the direction this is going.
The decision to go with concepts is interesting. It's more moving parts than I would have expected from a language like Go, which places a high value on minimalism. I would have expected concepts/typeclasses would be entirely left out (at least at first), with common functions like hashing, equality, etc. just defined as universals (i.e. something like "func Equal<T>(a T, b T) bool"), like OCaml does. This design would give programmers the ability to write common generic collections (trees, hash tables) and array functions like map and reduce, with minimal added complexity. I'm not saying that the decision to introduce typeclasses is bad—concepts/typeclasses may be inevitable anyway—it's just a bit surprising.
I've thought about that before, as I participated in one of the bugs where we chewed on these various matters, and I came around to the conclusion that in general, understanding what is in your current scope is just the cost of doing business. Being scope-based is the minimal cognitive load you can hope for.
I also strongly suspect that in the general case, you're not going to see more than one handler at the top of the function. It will be a rare function that has a dozen of them in there, and either A: it needs to be refactored, and while I'm sorta sympathetic to the idea that we shouldn't hand programmers too much rope to hang themselves with, that argument has the problem that you end up removing pretty much everything from the language or B: it's the core inner complicated loop of a program and it just needs that much complexity, which still doesn't mean it's going to show up in every function.
It's not that dissimilar from one function getting nested four layers deep in try/catch statements; yes, it would be a bit confusing to disentangle what that would actually do (moreso, since the catch statements are conditional but the handlers are not), but the answer is simply not to do that unless you really need to, and thwack any coworker that tries it on the wrist during code review. It is impossible to make a Turing-complete language in which I can't express something incomprehensible in.
Yeah, this is the obvious approach. Just make "check EXPR handle BLOCK" syntactic sugar for the "if err != nil" idiom. It's totally clear to the reader what's going on.
It's obvious because it's essentially just try/catch (as I'm sure you know) but misses the whole point of why they propose anything different.
The point of 'handle' is that it applies to ALL FUTURE calls to check in the function body. If we're going to consider alternatives we need to at least address the authors perspective (even if just to deny it).
I guess the proposal's central idea is that once you see an error, error handling is going to be focused on rolling back various stages of partial progress. It's the complement to how 'defer' incrementally accumulates guaranteed execution in the face of either a return or unwind.
That said, how common this issues is and if it's worth optimizing for is more up to debate. It would be nice to see a lexicon of well handled errors; for 90% most of what I do log and unwind is sufficient.
What do I know -- I actually like Java style checked/unchecked exceptions and think they'd work well for Go as the parent said. For what I'm up to it's often sufficient to just not leak partial progress via side effects. All the partial progress just evaporates into the GC. 'finally' or 'defer' takes care of most of the .close() or .unlock() calls.
The change I'd make to Java-style exceptions if I had a time machine is to make all exceptions types unchecked. Instead of looking at exception's types at all, enforce checks only if a function has 'throws' declared.
Calls to 'void close() throws IOException' would still need to be checked but a function containing 'throws new IOException()' isn't forced to declare throws or catch it. This lets API's be explicit and force due diligence but lets user code intelligently allow unwinding until it's caught at the appropriate level. Some sugar for code to acknowledge the checked error and unwind could be nice I suppose.
I think fattening out Go's err's to full exceptions with stacktraces is more pressing personally.
As someone that also likes them, I would point out that actually Java design team took their inspiration from CLU, Modula-3 and C++ regarding checked exceptions.
Im only familiar with Java and python, do CLU or Modula-3 do much differently? I heard C++ engineers generally hate exceptions, partially because they're unchecked.
The point being that CLU, Modula-3 and C++ had checked exceptions before Java was even a thing.
CLU never went beyond university projects, and Modula-3 research died after the labs went through several acquisitions, DEC => Compaq => HP.
As for C++, checked exceptions never gained much support, because their semantics are different from what Java adopted. Throwing an exception that isn't part of the list terminates the application and there were some other issues with them, so now as of C++17 they have been dropped from the standard after being deprecated in the previous ones.
In general there are two big communities in C++, the ones that turn everything on (RTTI, exceptions, STL, Boost...) and those that rather use C but get to use a C++ compiler instead.
So no we don't hate exceptions, it just depends on which side of the fence one is.
Yeah, the control flow of "handle" is the most questionable thing, from my standpoint. (Incidentally, I find the function-scoped nature of "defer" unfortunate too.)
handle/check looks like try/catch in reverse order with check/try limited to statements instead of code blocks. Not sure if that's an improvement or just different for the sake of being Go.
I don't think the Go 2 generics proposal amount to type classes;
From what I inferred from the proposal, contracts are structurally typed rather than nominally such that you don't have a location at which you explicitly denote that you are implementing something. Rather, the fact that a type coincidentally has the right methods and such provided becomes the implementation / type class instance.
Also; I didn't check this in a detailed way, but does contracts as proposed have the coherence property / global uniqueness of instances? I would consider that a requirement to call a scheme for ad-hoc polymorphism on types "type classes". In other words, Idris does not have type classes but Haskell and Rust do.
I dislike concepts because it seems to be a half measure; one that is already causing them pains in the design.
For example the issues with, implied constraints, infinite types, and parametric methods could be solved by using something more properly founded in type theory. Concepts are effectively refinement types[1] and a properly founded implementation from type theory would mitigate these issues.
I mean something properly founded in type theory, like say _refinement types_ as implemented in liquid haskell: https://www.microsoft.com/en-us/research/wp-content/uploads/... . Constraints of course are _not_ refinement types, but very close to a _subset_ of refinement types. Specifically they have deep similarity to Refined Type Classes from the above paper.
Having used Idris, LiquidHaskell, and F* to implement provable algorithms for some distributed systems I _don't_ think Go should go down that route. The techniques are simply not simple or clean enough for more general purpose languages. But having a type system which has a subset of those feature, and which stands on firmer mathematical firmament lets you reason about stuff like "which of these two programs is okay:
```
// OK
type List(type T) struct {
elem T
next List(T)
}
// NOT OK - Implies an infinite sequence of types as you follow .next pointers.
type Infinite(type T) struct {
next Infinite(Infinite(T))
}```
Which the go authors see as problematic:
> "It is unclear what the algorithm is for deciding which programs to accept and which to reject."
Idris certainly has no problem choosing which ones should and should not be used.
I don't think there is any lack of clarity about your List example; that is clearly forbidden. The example that is less clear is the one that generates a million types and then stops.
A similar issue arises in C++; the C++ standard says that there is an implementation defined limit on the total depth of recursive instantiations. Perhaps Go should do something similar.
C++'s method is certainly a practical way to limit expansion on inductively defined data types. I think a better way would be to construct a type system _opposite_ of most constructed today (which focus on the ability to express problems) and instead intentionally limit the types we are able to construct to those with "easy to use" and computationally efficient properties.
Consider this sample. We want to be able to define inductive types such as `List(T)`, but not `InfiniteList(T)` or `BigArray(T)`. While `BigArray(T)` is an interesting construct (it's effectively a dependent type)* it jumps past the "can I keep it in my head" smell test for me. As soon as a I have to reason deeply about what a type constructor does it just doesn't feel like Go to me.
So we want to be able to construct types which are inductive but can only calculate a single type. List{T} calculates one type, BigArray calculates _n_ types, and InfiniteList calculates an infinite number of types.
* In Idris one would write something like:
data BigArray : (n : Nat) -> (l : List m) -> Type where
Z l : Vect Z v
n l : Vect n l
Although I don't think they called out refinement types as a possibility (I only skimmed near the end), I wouldn't be surprised if they quickly determined that tackling undecidability is not a goal of the Go type system. Especially since every attempt at refinement types (or even nontrivial constraints) ends up with a compiler that gets really slow, and fast compilations are one of the tenets of the language.
Go contracts as-written, or "accept a T that can do a thing and have a default implementation", is orthogonal to refinement types.
N.B. I'd really love to see refinement types break past the acceptable threshold of compilation time, since they're really neat and are easy to explain to people in terms of their usefuless.
My thoughts here are based on this section of the generics draft:
> We would like to understand better if it is feasible to allow any valid function body as a contract body.
A generic function to compute a type is _very much_ in the realm of both refinement and dependent types. The BigArray example (as mentioned in a sister thread) is just a dependent type.
> The hard part is defining precisely which generic function bodies are allowed by a given contract body. .. We are most uncertain about exactly what to allow in contract bodies, to make them as easy to read and write for users while still being sure the compiler can enforce them as limits on the implementation. That is, we are unsure about the exact algorithm to deduce the properties required for type-checking a generic function from a corresponding contract.
My proposal/goal here is to define a type system, or set of constructors which has nice, formally provable limits on what can be constructed. Those constructs should be both easy to grasp _and_ computationally simple. As you say _general_ refinement types are not suited for a compiler focused on speed; however, a subset can very well be.
In some dependently typed languages they can be easily implemented via refinement types, where the “refinements” are the implementations of the typeclass requirements. Idris is a good example of this approach.
It has the advantage of making typeclasses first-class, and of enabling a lot of additional functionality without additional fundamental constructs. It is however very antithetical to what someone means when they say Go is minimalistic.
The decision to go with concepts is interesting. It's more moving parts than I would have expected from a language like Go, which places a high value on minimalism. I would have expected concepts/typeclasses would be entirely left out (at least at first), with common functions like hashing, equality, etc. just defined as universals (i.e. something like "func Equal<T>(a T, b T) bool"), like OCaml does. This design would give programmers the ability to write common generic collections (trees, hash tables) and array functions like map and reduce, with minimal added complexity. I'm not saying that the decision to introduce typeclasses is bad—concepts/typeclasses may be inevitable anyway—it's just a bit surprising.