Hey! I would like to join the discussion and add my 5 cents here, since I have been criticizing the contracts draft and I would like to show what were my points in order to support the current design draft.
I believe the original problem for the generics is to allow the same function to work on values of different types. So that You do not need to write something like AddInt
, AddInt32
, AddInt64
and so on. This is an example of situation when we would like to write something like Add(T)
and make that T to be one of the list: int
, int32
, int64
- all of the types what we have the Add
function defined for.
In this case Add(T)
acts much like an expression evaluating to a different function depending on the T
.
This can be called “parametrization by type” or “type parametrization” and I believe it is an exceptionally simple and straightforward way to do it.
Let me say a bit more about problems where need of contracts arise.
When we are writing in a context of type-parametrized function something like:
var a []T = []int{1, 2, 4}
a[2] = 3
It is okay to write []T[2] = T
(assign a value of type T
to an element of array of type T
by index 2) and we do not need to care what is T
.
But when we change a bit:
var a T = []int{1, 2, 4}
a[2] = 3
It becomes hard to judge whether we are allowed to write T[2] = 3
(assign 3 to an element of type T
by index 2). In order it to work, we need to state somehow [2]
is allowed for T
. There are a few ways to achieve it and the first one is the contracts:
type T interface{
require(a, b) {
a[2] = b
}
}
Okay, now, let’s say, we are allowed to write T[2] = 3
. But let’s try a harder example
var a T = []int{1, 2, 4}
a[2] = 3
a = append(a, 4)
This will not work with the previously defined contract T
, since a map[int]int
value also fits it.
Should we write something like this:
type T interface{
require(a, b) {
a[2] = b
a = append(a, b)
}
}
But what is the point in such syntax? How the compiler could guess and how exactly it should work? What if we write random garbage in the require
body, will it allow us to write it right in the code without any checks?
type T interface{
require(a, b) {
a *@#&@#$= b
}
}
var a T *@#&@#$= b
Of course, we can restrict it to allow using only operators like []
, ==
and so on, still how can we distinguish between a map and an array?
type T interface{
require(a, b, i) {
a[:] // only arrays and slices have this operator
i++ // i is an integer
a[i] = b // b is an element of array a
}
}
Is this enough to define what we want? Such a contract would also require the append
builtin to be changed, but I believe it will only clutter up the implementation with unnecessary type assertions.
These “requires” act very much like predicates and are just tricky way to “shortly” define a list of types. That is to say, a++
means any type having a ++
operator defined for it no matter how exactly (which is a problem itself). Anyway, it is very easy, to define such a contract with a list, which is the second way of saying T[2] = 3
statement is allowed:
type Incrementable interface{
type int, int32, ...
}
Let’s get to Your example:
type structField interface {
type struct { a int; x int },
struct { b int; x float64 },
struct { c int; x uint64 }
}
It is clearly visible, that we need to parametrize by a single field type:
type Custom interface{
type int, float64, uint64
}
type structField(type T Custom) interface {
type struct { a int; x T },
struct { b int; x T },
struct { c int; x T }
}
Depending on the context, we also can write it like this:
type structField(type T Incrementable) interface {
type struct { a int; x T },
struct { b int; x T },
struct { c int; x T }
}
And even further, we can consider such a change:
type structField(type T) struct {
a int
x T
}
And such a refactoring becomes now a pattern.
Instead of writing
type T interface{
type map[int]string, map[int]bool
}
We almost always should write:
type Z(type T) map[int]T
It is a way stricter way of defining not only “contracts” but also types allowing us to perform such things like appending elements to arrays, taking element of a map by its key, accessing a struct field and much more while remaining generic enough.
Thanks for the attention, I hope it helps.