Go 2 Generics: A way to make contracts more readable for people (if not programs)

November 28, 2018

In the Go 2 generics draft design, my only real issue with contracts is their readability; contracts today are too clever. Contracts have been carefully set up to cover the type constraints that people will want to express for generics, and I think that something like contracts is a better approach to type constraints for generics than something like interfaces (but that's another entry). Can we make contracts more readable without significantly changing the current Go generics proposal? I believe we can, through use of social convention and contract embedding.

For being read and written by people, the problem with contracts today is that they do not always clearly say what they mean. Instead they say it indirectly, through inferences and implicit type restrictions. To make contracts more readable, we need to provide a way to say tricky things directly. One obvious way to do this is with a standard set of embeddable contracts that would be provided in a package in the standard library. Let's call this package generic/require (because there probably will be a standard generic package of useful generic functions and so on).

The require package would contain contracts for standard and broadly useful properties of types, such as require.Orderable(), require.String(), and require.Unsigned(). When writing contract bodies it would be standard practice to use the require versions instead of writing things out by hand yourself. For example:

contract Something(t T) {
  require.Unsigned(t)
  // instead of: 1 << t
}

Similarly, you would use 'require.Integer(t)' instead of any of the various operations that implicitly restrict t to being an integer (signed or unsigned), and require.Numeric if you just needed a number type, and so on. Of course, contracts from the require package could be used on more than direct type parameters of your contract; they could be used for return values from methods, for example.

The obvious advantage of this for the reader is that you have directly expressed what you require in a clear way. For cases like require.Integer, where there are multiple ways of implementing the same restriction, you've also made it so that people don't have to wonder if there's some reason that you picked one over the other.

It's likely that not everything in contracts would be written using things from the require package. Some restrictions on types would be considered obvious (comparability is one possible case), and we would probably find that require contracts don't make other cases any easier to read or write. One obvious set of candidates for require contracts are cases where there are multiple ways of creating the same type restriction.

(In the process the Go developers might find that there were gaps in the type restrictions that you could express easily, as exposed by it being difficult or impossible to write a require contract for the restriction. For example, I don't think there's currently any easy way to say that a type must be a signed integer type.)

Use of the require package would be as optional as the use of Go's standard formatting style, and it would be encouraged in much the same way, by the tacit social push of tools. For instance, 'gofmt -s' could rewrite your contracts to use require things instead of hand-rolled alternatives, and golint or even vet could complain about 'you should use require.<X> instead of ...'. Faced with this push towards a standard and easy way of expressing type restrictions, I believe that most people would follow it, much as how gomft has been accepted and become essentially universal.

An open question is if this would add enough more readability to significantly improve contracts as they stand. I'm not sure that a number of the trickier restrictions in the full draft design could be implemented in this sort of reusable contract restriction in a way that both is usable and is amenable to a good, clear name. People may also disagree over the relative readability of, say:

contract stringer(x T) {
   var s string = x.String()
   // equivalent:
   require.String(x.String())
   // or perhaps:
   require.Returns(x.String, string)
}

At a certain point it feels like one would basically be spelling out phrases and sentences in the form of require package contracts. I'm not sure that's a real improvement, or if a more fundamental change to how contracts specify requirements is needed for true readability improvements.

Sidebar: Augmenting the language through the back door

The require package would also provide a place to insert special handling of type restrictions that can't be expressed in normal Go, in much the way that the unsafe package is built in to the compiler (along with various other things). People might find this too awkward for some sorts of restrictions, though, since using require has to at least look like normal Go and even then, certain sorts of potentially implementable things might be too magical.

Written on 28 November 2018.
« (Open)SSH quiet connection disconnects in theory and in practice
Go 2 Generics: Interfaces are not the right model for type constraints »

Page tools: View Source, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Wed Nov 28 02:01:13 2018
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.