Hacker News new | past | comments | ask | show | jobs | submit login
Scrapping Contracts: a critique of the Go generics proposal (merovius.de)
86 points by Merovius on Sept 5, 2018 | hide | past | favorite | 26 comments



I think this proposal looks pretty good, but it does expose something odd about the core language. Given this example from the article:

    func Max(type T ordered) (a, b T) T {
      if a < b {
        return b
      }
      return a
    }
It would make me assume that if I were to write a function that looks like this, it would at least accept the same type of arguments:

    func AnotherFunction(a, b ordered) {
        ...
    }
As in, substituting 'T' with 'ordered'. Unfortunately, it would not. In this case, the arguments are interface objects, which are pointers under the hood. Calls to the function have to be adjusted to pass in pointers. I think Go would have been more consistent if instances of interface objects always had to be prefixed by * . Concretely, this would throw a compiler error:

    func ReadStuff(r io.Reader) {
        ...
    }
Instead, it should be written as follows:

    func ReadStuff(r *io.Reader) {
        ...
    }
As in, r is a pointer to an object that implements the io.Reader interface.


This is complicated by the fact that a struct method can have a pointer or a non-pointer receiver. They can't be mixed, but consider the following:

    type Foo struct {}
    
    type Bar interface {
        Baz()
    }
You could implement the interface either with a pointer receiver:

    func (f *Foo) Baz() {...}
Or with a non-pointer receiver:

    func (f Foo) Baz() {...}
Pointer receivers are more common for various reasons, but non-pointer receivers are still perfectly valid.

If you force interface arguments to be prefixed with a * then you end up with confusing usage depending on how you initialize the variable being passed and whether the interface was implemented using pointer receivers or not.

eg, for your ReadStuff example above, there are the following possibilities:

    reader := SomeReader{}
    pointerReader := &SomeReader{}
    
    // SomeReader implements io.Reader with a pointer receiver
    ReadStuff(&(&reader)) // This isn't actually valid, compiler will throw "cannot take the address of &reader"
    ReadStuff(pointerReader)
    // SomeReader implements io.Reader with a non-pointer receiver
    ReadStuff(&reader)
    ReadStuff(*pointerReader)


> It would make me assume that if I were to write a function that looks like this, it would at least accept the same type of arguments:

    func AnotherFunction(a, b ordered) {
        ...
    }
Not really. If we consider `ordered` an interface, then `func(a, b ordered)` does not enforce typeof(a) == typeof(b), only that both types implement `ordered`.


Exactly, which is why I said: "it would at least accept"


This is, in my opinion, the best critique of the Go generics proposal so far. There's more discussion on Reddit: https://www.reddit.com/r/golang/comments/9d1zsz/scrapping_co....


I've pinched your description for the title above. (I just know we'll get complaints if we leave it as "Scrapping Contracts".)


The Go community is against the proposed implementation and has provided a well thought out alternative. If the past is any indication, the community proposal will be rejected and we'll have the original proposal that no one is really happy with.

It's fun to dream though.


What? Several people expressed constructive critique. You extrapolate this way too much in this comment. I, for one, am part of "Go community" and am certainly not against the proposal. More than that, I love that people smarter than me are currently publicly discussing and exploring alternatives and disadvantages in a civil manner.


I also felt uneasiness with the definition of contracts but couldn't put my finger on the reason. Merovious made now clear to me that the language to define contracts is the problem.

Nevertheless, I do think we need to keep contracts because they are a superset of interfaces. Interfaces specify only a collection of methods, and contracts specify a collection of methods and operations (e.g. ==). Remember that base types can also have methods. We may also want to restrict types to referenced, or unreferenced, because it determines if an assignment make a copy or not.

Contracts are types of generic parameters. I would find more natural to define these generic parameter types like this

    type foo generic { ... }
As suggested by Merovius, the content would be a list of method signatures, operations, interface names or predefined operation set names.


> methods and operations

Isn't a major part of the idea in the article that we can replace the "and operations" part with a system of pseudo-interfaces that encompass those basic operations?


To me, the most interesting part of this whole Go2 discussion is the amount of brainpower being expended on keeping the language on its current path of pragmatic minimalism and not careen off into type system land akin to contemporaries like Rust, C#, Scala and Swift, while at the same evolving and modernizing it. It's not easy.

Not that complex and rich type systems are a bad thing, but there's something to be said for Go's austerity. Having been a Delphi/ObjectPascal programmer for many years back in the 1990s, Go feels very familiar to me, with good approaches to many challenges. I do think Go's designers made some crucial errors that should have been obvious at the time, and that we are paying for them now. In attempting its special flavour of minimalism, they arguably didn't bother to learn enough from past language experiments. Of particular note, Modula-3 has a very pragmatic approach to generics [1], which later inspired similar approaches in Delphi/ObjectPascal/FreePascal. (I'm not saying Modula-3's design is right for Go, though.) If you want to look at extreme engineering pragmatism (as opposed to type-theoretical elegance you find on Idris, Agfa etc.), look at Ada, or even plain Pascal. Both have range types and enums and other things which grant the language first-class mechanisms for tightening a program's semantics, but they do so in the syntax and compiler, not through advanced type system shenanigans. (Oberon, which influenced Go in many ways, actually did away with enums in the name of simplification. Minimalism can go too far, too.) Ada, arguably the most engineering-oriented language ever conceived, also has tagged unions, although I don't think they were ever used for error returns, since Ada has exceptions.

I'm not at all worried that Go adopting generics will lead to type system madness, especially given how conservative the designs have been so far. I think the amount of discussion and lively idea brainstorming in the community right now is a very healthy sign. More than some other language communities, there have been times when it's seemed -- right or wrong, it's hard not to get this impression from the vantage point of an ordinary developer -- that the Go team has historically been somewhat distant, preferring to unleash finished implementations on the world instead of evolving their designs in collaboration with the community (cough vgo vs. Dep), and I think this is an opportunity to open up the process a bit. I don't know about anyone else, but I'm certainly very much enjoying all these articles being put out.

[1] https://www.cs.purdue.edu/homes/hosking/m3/reference/generic...


> More than some other language communities, there have been times when it's seemed -- right or wrong, it's hard not to get this impression from the vantage point of an ordinary developer -- that the Go team has historically been somewhat distant, preferring to unleash finished implementations on the world instead of evolving their designs in collaboration with the community (cough vgo vs. Dep)

I think one reason may be that "constrained minimalism" and "internet community" are two things that don't really play nice together.


I don't mean this as a criticism, but it's amusing how the Go community is slowly inventing Swift.

- "Protocols" that can be used as both constraints on generic parameters, and existential types

- Operator requirements in protocols

- Associated types

All of the above introduces a huge amount of complexity too, and I imagine trying to retrofit it onto an existing language that does not have generics is going to be an interesting challenge.


To be fair, Swift is on another level, and doesn't strive for Go's rather relentless minimalism. Go, even with these additions, would be a lot smaller than Swift, which was intentionally designed around rich, complex abstractions, including generics and compile-time type constraints, from the start.

Of course, Swift didn't exactly pioneer these concepts. Scala, C# and Haskell also provide compile-time type constraints that are heavily based on generics, and these languages also influenced Swift.


"Go's rather relentless minimalism."

Semantic minimalism, yes. But the produced artifacts are not "minimal". Go is something like IKEA of PLs, making hay with a "minimal" set of elements to play with. But there is only so far you can take plywood.

And above is not a negative critique. It is a good niche to be in. But this effort for Go 2 to move upmarket (in PL land) using the plywood foundation is rather worrisome.


It's always been "curious" that Swift never credited Scala, I thought.


Because these concepts are older than Scala and both languages inspired from the same sources?


My impression when I first saw swift code was "wow, that looks a lot like scala".


Anecdotally as a mostly full time scala dev, swift has been the easiest "I'm bored this weekend I'll pick up a new language for side projects" I can remember.


The 10,000 foot view of there being many distinct industry-grade language platforms:

Duplication of Effort


All these proposals are far too explicit and cause too much boilerplate code, someting that the Go team so far tried to avoid (or so it seems).

I‘d prefer a macro-like approach to generics where generic parameters are unconstrained like interface{} until they are used inside the function with constrained operators/function calls, which define the limitations for the parameters. Just like a macro that expands to code that only works with particular types, it will simply generate an error when the compiler detects an incompatibility between the types used by the caller and the operations inside the function. This would be powerful enough and simple to understand.


> I‘d prefer a macro-like approach to generics where generic parameters are unconstrained like interface{} until they are used inside the function with constrained operators/function calls, which define the limitations for the parameters.

That sounds awfully like how C++ templates work, and I think that it’s pretty much universally agreed that that approach is not ideal due to the potential for supremely terrible error messages (among other drawbacks, probably). Contracts allow for type checking at the call site, which is significantly more user-friendly.


Compiler errors and their quality are fundamentally an implementation issue. I‘m talking about the design of the language and what the user has to deal with all the time, not just in the error case. Designing primarily for beautiful error messages is a flawed concept IMO.


> Compiler errors and their quality are fundamentally an implementation issue.

I don't really agree here; language design can place constraints on the quality of errors emitted by implementations. There's a reason Concepts are such a widely desired feature in C++, even though template errors now are vastly better than they were in the past --- there's only so much you can do with the language in its current state.

> I‘m talking about the design of the language and what the user has to deal with all the time, not just in the error case.

I'm inclined to say that fixing/avoiding type errors is likely to be far closer to what programmers have to deal with all the time than having to write contracts, especially for library types.

In addition, it's not just the error case that's involved here. If you're writing generic code with constraints on the involved type(s), those constraints are probably going to have to be documented somewhere anyways. Why not write them in your source code where the compiler can help?

> Designing primarily for beautiful error messages is a flawed concept IMO.

I'm not saying that the design has to be primarily for beautiful error messages; it's just that useful error messages are important and whatever design emerges should try to enable helpful error messages.


I'll admit that I don't have much experience with generics, but can't we get done what we're trying to get done just by letting builtin operators get overloaded, then making an interface for those functions and using the interface? From what I've seen, it appears the main point is to let various types that can all get "+" applied to them get handled with one function. But defining the interface is the contract and the interface is any type that fulfills it, so...? What am I missing?


I think the graph example of the article, instantiated using an adjacency matrix, shows nicely a need for generics that has nothing to do with operators.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: