Description
Problem Statement
Currently, interface types and composites of interface types do not implement the comparable
constraint interface.
Yet, the use of the comparison operators (==
and !=
) is allowed for interface types (and composites) in non-generic code.
We would like to be able to use those operators in generic code as well.
It should allow us to use interfaces such as reflect.Type
or ast.Node
in generic Set or Map datastructures by default.
Proposed Solution
We need a new constraint that is not an interface, for reasons we explain below.
This might well be a standalone unless there are other interesting operations available to interface types, that are shared only by some but not all members of the typesets they determine.
A quick, non-committing, idea would be to make it a predicate constraint e.g. HasComparisonOperators
(too lenghty a name!!)
It should solve issue #52474 and permit the definition of generic Sets and Maps accepting interface types as type parameter values.
Explanation
For a summary of the issues at stake, skip to: #52624 (comment)
Go1.18 introduced constrained parametric polymorphism in the language.
The type constraints that can be written in 1.18 are under the form of interfaces.
Interfaces in Go denote set of types called type sets that can be defined as the set of non-interface types that implement said interface. Type set inclusion and interface implementation are in practice linked in an equivalence relation.
These type sets should mirror the subtyping relation of interfaces by being transitive sets meaning that:
Given three interfaces A
, B
, C
such that A
implements B
and B
implements C
,
we should be able to infer that typesetOf(A) ⊂ typesetOf(C)
(equivalent to A
implements C
).
This was always the case for pre-1.18 Go when we only had Basic interfaces. This should extend quite naturally to all interfaces in Go 1.18
We posit that this property of typesets (transitivity), along with the equivalence relation between interface implementation and typeset inclusion , is very advantageous if not necessary for their accurate computation as well as the computation of the operation set and/or method sets, available to the members of typesets.
We also claim that the current issue of comparability/availability of comparison operators stems from the fact that it does not follow the subtyping relation.
If we take the simple example of the any
interface, function types implement any
as they are members of its typeset.
However they do not possess the comparison operators. Yet any
, as an interface type, can still use the comparison operators.
It shows that an interface type having access to the comparison operators cannot be inferred from its typeset. The operations of interface types are independent from the operation set defined by the respective typesets.
So what now?
We need a new way of defining the permissible set of types that defines a type constraint.
This new kind of set should not be defined by an interface (so that it does not get embedded or it might interfere with the typeset computations). Such a set will be able to directly include interface types (and composites). Therefore, it won't have a type set.
As shown above, inducing operations of interface types from their type set is insufficient in the case of comparability. It only works for the current implementation of comparable
and its embeddings.
Do we still need the comparable
interface constraint or #51338 ?
We may not need it as much anymore but it may not hurt to have it either (see #49587 ).
Besides, comparable
should belong to the set denoted by HasComparisonOperators
by definition.
What about the interactions between two kinds of constraint?
Since HasComparisonOperators
may only be used in generic code to constrain generic map key types for example, that's where we should place our focus.
An example of interaction is the case of nested functions where the inner function uses HasComparisonOperators
and the outer function uses interfaces as constraints for a same type parameter.
It's presumed that HasComparisonOperators
should apply further bounded quantification to the typesets accepted by the outer function.
Said more simply, the set of permissible types for the outer function has to be a subset of the set defined by HasComparisonOperators
.
Shouldn't we be able to combine the two kinds of constraint?
A priori, there would be no need for it. So no need to create extra syntax.
We don't think that there are many cases where one wants a subset of all types satisfying HasComparisonOperators
since, typically, the sole constraint on map key types is to have the comparison operators.
Maybe someone can come up with a rebuttal use-case though.
[edit] @Merovius found a rebuttal-case.
We need to find another way equivalent to interface embedding to combine constraints.
That should be easy enough.
A type parameter definition could accept a list of constraints. For instance:
- space separated constraints for a type parameter would create the conjunction of constraints i.e. the intersection of the set of permissible typês for each constraint
- Or we could use boolean connectives to make the conjunction more apparent (more future-proof perhaps)
- ... other ideas ?
That could look like this:
func F1[T comparable fmt.Stringer](a, b T) bool { return Eq(a,b) // ok }
func Eq[T comparable](a, b T) bool {
return a == b
}
// OR
func F1[T fmt.Stringer & comparable](a, b T) bool { return Eq(a,b) // ok }
func Eq[T comparable](a, b T) bool {
return a == b
}
Why not just change comparable
?
[edit] we could by using the provision from the backward-compatibility promise.
"comparable" is a good name. But for the aforementioned reasons that changing comparable
would interfere with typeset computations, we think that comparable
should retain its current semantics.
Creating a new kind of constraint might add a bit more complexity to the backend. But the advantage is that it would not interfere with typeset computations, leaving us with some leeway to use all interfaces as types later.
This could be a crucial point in the discussion about sum/union types for instance.
The alternative* could more easily preclude us from entertaining using interfaces as union types (#19412), should we see a need for it (because typeset calculation would not be precise). It might be better to leave the design space open, just in case.
(*) the alternative as proposed by some would be to change the comparable
interface. It would still be an interface constraint :
As such it would be embeddable but reifying it into a type would not be possible.
It would create two subcategories of interface constraints : those that embed comparable
and would not be reifiable into types and the rest that would be reifiable.
Other naming ideas?
weakcomparable
comparable-maypanic
comparable-unsafe
- ...
We could also change the implementation of comparable
so that it is equivalent to our HasComparisonOperators
In which case we would take advantage of the provision in the backward-compatibility promise.
Changes to the Specification
Just a few notes, leaving that part to the more experienced.
- A constraint
C
would be said to be satisfied by a typeT
if(f?)T
belongs to the set of permissible type arguments defined byC
. - interface implementation would be equivalent to typeset inclusion. Note: that does not mean that if I implements A, I is in the typeset of A. It means set inclusion of the corresponding typesets.
- the definition of a type being comparable should be adjusted for the semantics of the current implementation of
comparable
- there would be effectively two ways to define a type constraint: interface constraints and predicate constraints. Both defining a set of permissible types.
- type parameters for generic map keys could use the predicate constraint
HasComparisonOperators
or be more restrictive by using the interface constraintcomparable
. The choice would be left to the programmer.
References
G. Castagna, T. Petrucciani, and K. Nguyen (2016) Set-theoretic types for polymorphic variants
Jech, Thomas J., et al. Set theory. Vol. 14. Berlin: Springer, 2003.