Go 2 Generics: A way to make contracts more readable for people (if not programs)
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.
|
|