The proposal extends the Go 2 contracts (generics) draft design in four ways:
The gist of it is that the use of contracts should be explicit. For this contracts should act as their own adapters. Fundamental and custom types can be coerced to behave in a common way. The proposed contract switch is essential for this. Contracts need to become real executable code with a few special rules.
Contracts can declare their intended outcomes. They become real code for adapting types to this outcome:
contract WithLength(a T) int {
return len(a)
}
Contract outcomes can be applied directly:
func Max(type T Less)(first T, other ...T) T {
max := first
for _, val := range other {
if Less(max, val) {
max = val
}
}
return max
}
var x = Max(5, 3, 8)
Contracts with no declared outcomes cannot be applied.
As types can abide multiple contracts it is helpful to know which contract is applied where. Using the contract name for that purpose led to a design with contracts becoming their own adapters.
Contract switches react on the semantics of their case statements.
contract Less(a,b T) bool {
switch {
case: return a.Less(b)
case: return a < b
}
}
Case statements with semantic errors are silently ignored. Syntax errors are treated as always. The first applicable case statement is taken.
A type is considered to abide a contract if there is a valid code path to an explicit return statement. Contracts without declared outcomes don't need the return statement though. This allows contracts to steer types to behave in the most suitable way if any.
In contracts the control flow must not change outside of contract switch case statements.
Contracts may customize error reporting for specific cases. A code path that ends in a panic() reports its message as error:
contract Hash(t T) uint {
switch {
case: t = []byte{}; panic("byte slice hashing is not supported yet")
case: return t.Hash()
case: return uint(t)
}
}
Conformance to multiple contracts is a common trait of types. Requiring extra contracts just to combine them is bothersome, so a syntax for declaring multiple contracts per generic type is proposed:
type HashMap(type K Equal*Hash, type V) struct {
buckets [][]struct{key K; val V}
}
contract Equal(a, b T) bool {
switch {
case: return a.Equals(b)
case: return a == b
case: return string(a) == string(b)
}
}
contract Hash(t T) uint {
switch {
case: return t.Hash()
case: return uint(t)
}
}
I love the idea behind "Contracts Switches", as it solves one of the concerns I've always had since the first Generic draft: the unification of native types and custom types with regard to operators.
The only thing that I still don't get to like is the need to use adaptors to leverage this, as it leads to code that you would normally not write. For example, the
Max
function of your example would have been normally written using the<
operator.May I suggest a simplification and a restriction to the
switch
statement?The idea is to remove the returned type in the contract and make the switch statement to define "equivalent statements/expressions". Let me write an example using the new Generics draft design (although we are using operators, something that the latest design moved away from):
The compiler will ensure that the expressions/statements in each branch are equivalent, meaning that they can be used in the same situations (I would say this only includes ensuring the returned type is exactly the same, but further thinking is needed)
This way, you can write generic code using any of the equivalent statements. Your
Max
function would be:This will be very useful (the unification of operators in native types with methods in custom types) in a lot of situations. It can also be used as way to do operator overloading but simpler!
Thanks for posting this!