5 ms·
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
by pcwalton 8y ago
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.
- the_duke 8y agoI don't like the possibility of nested handle() blocks. This could make grasping the control flow in a function very cumbersome. Everything else looks like a very good change to me and would draw me back in to Go.
- pcwalton 8y agoYeah, the control flow of "handle" is the most questionable thing, from my standpoint. (Incidentally, I find the function-scoped nature of "defer" unfortunate too.)
- kenhwang 8y agohandle/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.
- timClicks 8y agoI have noticed that my Python code increasingly works like this via the optional else clause in its try/except mechanism.
- jerf 8y agoI'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.
- the_duke 8y agoI actually like the explicit error handling idiom a lot. (especially in Rust now that is has been made convenient with the ? operator and the failure crate which provides backtraces and wrapping). I would prefer an extension of the `check` syntax instead of the standalone handle blocks. For example: check someFn() |> if err : = someFn(); err != nil { return err; } check someFn() handle { panic(err) } |> if err := someFn(); err != nil { panic(err) } So handle blocks are only for an individual expression. This way the control flow is still linear and explicit (ignoring defer...) but the syntax is much more convenient.
- pcwalton 8y agoYeah, 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.
- zbobet2012 8y agoI 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. [1] https://en.wikipedia.org/wiki/Refinement_type https://en.wikipedia.org/wiki/Refinement_type
- phillipcarter 8y agoWhat "properly founded implementation" would you suggest? Concepts are not "effectively" refinement types.
- zbobet2012 8y agoCombative a bit are we? 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/2016/07/LiquidHaskell_ICFP14.pdf 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.
- ianlancetaylor 8y agoI 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.
- chombier 8y agoI'm also surprised, manual dictionary passing ("scrap your typeclasses"-style) seems way simpler.
- centril 8y agoI 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.