(This is a refinement of Burak Serdar's proposal at https://gist.github.com/bserdar/8f583d6e8df2bbec145912b66a8874b3)
The simplest way to specify a contract is to provide a list of the types that fulfill it:
contract byteSequence string, []byte
contract unsigned uint, uint8, uint16, uint32, uint64, uintptr
contract signed int, int8, int16, int32, int64
For any of the types listed that do not have methods, the contract will also be fulfilled by named types derived from it.
For example, time.Duration
fulfills the signed
contract.
In addition to types, an enumerated contract can also contain other contracts:
contract integer signed, unsigned
contract orderedPrimitive integer, float32, float64, string
A structural contract (so called because it operates on the principle of structural typing) is a lot like an interface, but it can also include fields:
contract reader {
io.Reader
}
contract lesser {
Less(b lesser) bool
}
contract linkedListNode {
Next *linkedListNode
}
In the body of a structural contract, contract names may be used as if they were types.
The most common case of this is when a contract contains a reference to itself,
like the lesser
and linkedListNode
contracts above.
In that case, the type that fulfills the contract must reference itself in the same way.
For example, the following myNode
type would fulfill linkedListNode
:
type myNode struct {
Val string
Next *myNode
}
It is also possible for contracts to reference each other, to define a more complex relationship between types:
contract Node {
Edges() []Edge
}
contract Edge {
Nodes() (from, to Node)
}
func ShortestPath(type N Node, E Edge)(src, dst N) []E
When a set of type parameters involves contracts that reference each other,
the types that are supplied must fulfill the contracts in a compatible way.
Each contract must be consistently fulfilled by the same type.
For example, the Edges
method of the type supplied for N
must return a slice of the type supplied for E
.
You can't use a Node
type from one type of graph, and an Edge
type from another.
(Note that in this proposal, contracts are per type, rather than covering the whole list of type parameters.)
There is one built-in contract, with the predeclared identifier comparable
.
It is fulfilled by types that support ==
and !=
, and can be used as map keys.
(In my first draft, I allowed using operators in structural contracts.
But I realized that the only one that adds any expressive power is ==
,
because the other operators are limited to a specific set of primitive types
(plus named types based on them).
So they can be specified by simply enumerating the types that support the operator.
And the contract for supporting ==
should probably be sort of a special case anyway,
since it should also allow for the use of !=
and using the type as a map key.)
A type switch can be used with a type parameter in the switch
line,
and contracts or individual types in the case
lines.
This allows writing generic functions that operate on types that may fulfill any one of several contracts:
contract ordered orderedPrimitive, lesser
func Min(type T ordered) (a, b T) T {
switch T.(type) {
case lesser:
if a.Less(b) {
return a
}
return b
case orderedPrimitive:
if a < b {
return a
}
return b
}
}