Scrapping Contracts: a critique of the Go generics proposal
blog.merovius.de
blog.merovius.de
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. 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) 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`.It's fun to dream though.
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.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?
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...
I think one reason may be that "constrained minimalism" and "internet community" are two things that don't really play nice together.
- "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.
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.
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.
Duplication of Effort
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.
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.
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.