Skip to content

proposal: spec: add new constraint kind satisfied by types that support == (including interface types) #52531

Closed as not planned
@atdiar

Description

@atdiar

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 type T if(f?) T belongs to the set of permissible type arguments defined by C.
  • 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 constraint comparable. The choice would be left to the programmer.

References

Alain Frisch, Giuseppe Castagna, Véronique Benzaken (2008) Semantic subtyping: Dealing set-theoretically with function, union, intersection, and negation types

G. Castagna, T. Petrucciani, and K. Nguyen (2016) Set-theoretic types for polymorphic variants

Transitive set. Wikipedia

Jech, Thomas J., et al. Set theory. Vol. 14. Berlin: Springer, 2003.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions