The problem with contracts

It does not provide a clean, systematic way of indicating which operations are allowed on a type. For example, two different contracts may describe the same idea in two very different ways (taken from this blog post):

They can also describe very different things even though they look similar. Consider this example:

contract add(t T) {
t = t + t
}
contract mult(t T) {
t = t * t
}

It looks like both would refer to numeric types, right? Not quite: add is also compatible with string.

But there is more: mult implicitly allow operations that would most likely not produce a compilation error:

func Halve(type T mult) (a T) T {
return a / 2
}

The whole point of contracts was that you state what you can do with a type outside of the function body. Will programmers be thorough enough in explicitly listing all the operations they need if some can be omitted? I don't think so.

On the contrary, it can and will end up as a game of one-lining constraints, akin to the (c = getchar()) != EOF && isalpha(c) ? toupper(c) : '\0' follies in C and C++:

Is it clear that pointer could be a slice or a map as well? How about magic, what could it even be? Will the compiler produce an error if a contract is impossible? The horrors of real-world code should not be underestimated.

A contract exposes the internals of types to the users: it can be both exported and require a given type to be implemented in some way only, which is for example a huge step back from the decision of leaving fields out of interfaces.

A substantial amount of the standard library and existing code from the outside world may end up being rewritten with contracts to only mimic what was done with interfaces until now, leading to projects with a random mixture of contracts and interfaces depending on each contributor's habit on the matter.

It is overall too clever, as formulated by Chris Siebenmann. He also adds:

Go contracts do not seem designed to be read by anything but the compiler. This is a mistake; computer languages are in large part about communicating with other people, including your future self, not with the computer.

Operators can only be used on the very small set of primitive types anyway: the amount of distinct, meaningful contracts that can be written with operators is thus very small. It means that in general, a contract will describe the methods and fields of a composite type, making it a lot like an extended, generic version of interfaces.

This is exactly what I am going to present next: an extended, generic version of interfaces.

A case study

Let's say we want to write a generic Min() function. It should probably look like this:

func Min(a T, b T) T {
if a.Less(b) {
return a
}
return b
}

It is ambiguous whether T refers to an existing type, or any type. We still want the compiler to report errors when mistakenly using non-existent types, so at least we need a way to indicate that T is a parameter as well. The syntax proposed in the current draft design is almost just fine:

func Min(T type) (a T, b T) T {
...
}

See how the position of type differs from the draft: by switching it at the end, it now looks like a function definition — which it is, since we use generic functions just like functions on types:

// More on that import later
import "types"
var minInt = Min(types.Int)

It eliminates the cognitive burden of using Min(types.Int) just like a function, but defining it a different way. In short, it just reads better.

But this is not all, it also enables two possible additions to the language in the future that would then fit naturally:

"Pseudo-"partial applications, by extending the idea of "chaining" parameter lists to build higher order functions:

In my opinion, it would look much better than the current draft design:

func Min(type T ordered) (a T, b T) T {
...
}

Note that in the current draft design ordered would be a contract, which means that we could very well still add contracts as "types" of types in the future if what I suggest next is implemented.

Now back to our example. We want to indicate that T should have a method called Less(), which takes an argument of type T and returns a boolean. If T was known, then what we would need is exactly an interface; it makes perfect sense to first attempt to extend the semantics of interface to something that works for any T. By reusing the same syntax, we could try:

The last case is possible if, and only if T == S which has no reason to occur in real-life situations and most probably is a bug, so it should raise an error at compilation anyway. The proposed syntax avoids this error, and repetitions as well.

For the same reason, the following code:

type Union(T, S type) interface {
T
S
...
}

should produce an error at compilation since, in general, T != S. That means a concrete interface must be associated with exactly one type parameter. If there are zero associated types, then basically it is a normal generic interface. If there are more than one, then it is an error.

What is the type of c? Of course, it should be http.Client. That means a concrete interface acts like an assertion on type properties at compilation: it takes a type as an argument, and returns it if it corresponds to the specifications, otherwise the compilation fails. This is why I strongly advise naming concrete interfaces with the prefix With to indicate that a particular property is expected. Our initial example would be better rewritten as:

A first major drawback is that you can't specify fields with an interface, which is a good design decision in my opinion. I assume that the initial philosophy was that when exporting an API, only how you can interact with an object matters, not its inner working. Contracts, on the other hand, allow exposing the internals to the users, and may restrict how a structure is implemented.

If yet, such a possibility is desired, we could naively think at first of some way using generic structures. Let's say, for a new example, that we want to write a generic ValueOf() function that takes a structure with a Value field, and return its value:

func ValueOf(T, S type) (x T) S {
return x.Value
}

Following the same logic than with interfaces, we would think of something similar to this:

... This time, WithValue(Integer, int) is not the same type as Integer! Indeed, it has an extra Integer field representing the embedded structure.

At this point, there are two solutions:

Abandon the possibility to specify fields, in accordance to the principle that the internals should not be fiddled with by the outside world

Extend the semantics of concrete interfaces again to indicate which fields are required, with the exception that exporting such an interface, or a function using such an interface is an error at compilation:

I strongly support this second option. Since interfaces containing field requirements cannot be exported, there is good practice to write separate, unexported interfaces containing field requirements only. It would still be very useful inside of a package to indicate constraints, and would prevent exposing the internals of the package when used by someone else.

The second drawback is what probably motivated the idea of contracts in the first place: operators. In our example with Min() I took great care of writing a.Less(b) instead of a < b, and for a very good reason: for as long as Go refuses to implement operator methods (I don't hold an opinion on that), there will be no choice but to write two versions of every function working on both composite and primitive types:

Whether contracts are implemented or not in the end, code duplication will still occur with primitive types. Since composite types are more frequent than primitive types, a generic function will most likely end up using methods rather than operators; therefore using methods should be preferred. Given this perspective, we should try to find a suitable solution with interfaces one more time, using what the language already permits.

As stated before in the preliminary criticisms of contracts, some operators are implicitly allowed (t * t implies t / t), others don't make sense when used together (for example t && t and t + t), and overall the set of meaningful contracts using operators is very small. An excellent idea brought forward by Matt Sherman, and named by Axel Wagner are "pseudo-interfaces". Instead of adding a way to list which operations are possible, we add a small set of builtin identifiers grouping operators that only make sense together:

With a set of sane, predeclared pseudo-interfaces, developers would only need to refer to the standard Go documentation to find out what they mean, instead of tediously trying to guess what a contract implies:

contract num(t T) {
t == (t - (t * t)) // Is it obvious?
}

A small issue arises when you think about how to write a generic Min() function again. We still need to know what type we're referring to, as ordered in our case is merely an interface. The code would then look like this:

At last, we still have the problem that every generic function will have to be written twice: one for the primitive types, one for the composite types. As a convenience, and for performance reasons, I would encourage the introduction of a new "pseudo-"package named types, which provides a number of structures and interfaces wrapping the operations on the primitive types:

Using dark magic with the compiler, we could natively translate types.Int into int at compilation, avoiding the extra overhead of wrapping everything into an actual structure with methods. The package don't really exist as source code, hence the prefix "pseudo". A number of generic structures could be provided by types as well:

If methods from types are assigned (for example f := m.Get) or the types from types are embedded to create substructures, the compiler could fall back to an actual implementation of the structures in pure Go. Since these types would be standard and supposedly widely used, we still may find ways to optimize such situations in the future, to the benefit of everyone.

types could also include concrete interfaces, being the general counterpart of the aforementioned pseudo-interfaces:

Interface

Definition

comparable

==, !=

types.Comparable

interface(T type) { T; Equal(T) bool }

...

That is, if type x is compatible with pseudo-interface p, then type types.X (notice the capital letter) is compatible with concrete interface types.P. This would break the rule of naming concrete interfaces with the prefix With, but at least it is memorable and coherent with everything else. We can write at last:

This comment has been minimized.

Making whole interface concrete is a big restriction IMO. Maybe it would be easier to introduce Self pseudo-type to interface declaration and make any method which uses it "concrete". Then, "concrete" methods cannot be used in dynamic context.

I'm not sure about "interface fields" part.

This comment has been minimized.

edited by ghost

@target-san I'm not exactly sure to understand what you mean, but "concrete" was just a word I came up with to describe the property of a generic interface to be associated to one of its type parameters. How it translates into an actual implementation is still to be worked on. In particular, it would be nice to be able to write type ReadCloser WithClose(io.Reader) and mean it : following a basic substitution rule should produce an interface identical to io.ReadCloser. My goal was to avoid coming up with new keywords, or notions that would cover part of what you can almost already do with interface, so I don't really know about "self pseudo-types".

EDIT. More on concrete interfaces: I see that I wrote they "should be read in code as an assertion on a type", but this is just a hint in how the programmer should think when they see one, not an actual rule on how they should be implemented. It's easier to remember that WithClose(T) means that we test whether type T has a Close() method rather than how things are operated in the background.

On the "interface fields" part I anticipated it would be hard to accept. This is why I suggested forbidding to export interfaces using them in order to sort of keep the original spirit of interfaces (which is in my opinion, as I state in my presentation, a good design decision from the original authors). Or, we could still set this aside and maybe make a decision in a later version of Go 2; and be content with generic structures for now. Maybe this would require a deeper analysis of the existing real-world codebase to decide whether it should be added or not.

This comment has been minimized.

@ianlancetaylor
I guess OP's main idea is "why have separate entity for compile-time interfaces?".

@kantbell
On the other hand, unification of interfaces and contracts looks a bit questionable. While they partially "overlap" semantically, they cover different sub-domains of language polymorphism. Such unification will require smth similar to Rust traits, which in turn are more powerful yet more complex than interfaces and traits combined. I doubt core team would pursue such goal.