-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: comparable as a case of a generalized definition of basic interfaces #53734
Comments
cc @ianlancetaylor @griesemer |
@ianlancetaylor thanks for the heads up. From a cursory look there are still a few differences that we can discuss in #56548 I will comment there in a few. |
This issue is on hold but because of a lengthy discussion in #56548 related to this issue, I reread this proposal. As is, this proposal is very vague. It crucially depends on how type sets are defined and interface and constraint satisfaction differ. (By contrast, #56548 leaves type sets as is and simply introduces an exception to the rule for constraint satisfaction.) This proposal suggests that constraint satisfaction is based on type membership. In order to understand which programs are permitted and which are not, it is crucial to be precise what the type sets are. Without a precise definition of how type sets are constructed per this proposal, we cannot further consider this. The prose talks about 3 possible ways, based on propositions, or a list of types, or a singleton type - none of it laid out in detail. The proposal should define exactly what is the type set of a non-interface type, of an interface type (which is not a type parameter), and a type parameter. Per this proposal, Per the "type set membership rule", func f[P comparable]() {
f[struct{ any }]()
} Now the question, is the following function valid? func g[Q struct{ any }]() {
f[Q]() // calling f from the example above: does Q satisfy comparable?
} That is, is the type If the program is valid, why? (I note that per #56548 this function is not valid because Q is not strictly comparable.) Without clear answers to all these questions, we cannot further consider this proposal - there's not enough here. Thanks. |
@griesemer Thanks for the feedback! . I'm on it. I haven't modified this proposal yet. Just to address one of your questions quickly: func g[Q struct{ any }]() {
f[Q]() // calling f from the example above: does Q satisfy comparable?
} Just as in #56548 it should not be valid. Since we are comparing the constraint themselves, this is in fact testing whether I will get to making clarifications shortly especially re. type sets. As you correctly remarked, just as in mathematics, I am thinking of determining type sets by sets of intensions/constraints/propositions. We don't have to prove that those set of constraints that define a type set are satisfiable for the most part. That would turn the problem into a boolean satisfiability problem just to make sure that all interfaces we define can be inhabited i.e. type sets are not empty. We just need to test whether a type can be a solution which is the application of set operations on constraint sets. Someone may want to double-check on that still. Anyway, I will ping back with clarifications. Thanks again. |
How can |
edit: admittedly this is a bit handwavy of an answer. This is something that may require further thoughts in terms of type sets. What is the type set of |
So just a few notes (in the meanwhile). I.There was indeed an inaccuracy but it's resolvable.
Where it might be a bit involving is to spec this in Go, perhaps. We can have a feel for them when creating a value such as A value of type As you rightly said, it is a bit more of a mathematical idea of type sets. It should still be tractable however. II.Another important note to answer why we can lift the restriction on type set membership of interface types. I think it was the right move to make that restriction out of caution initially. So it's fine for Thankfully you challenged, product types had escaped me! |
@griesemer @ianlancetaylor updated the proposal. Hope it helps. Many thanks. |
Apologies for being blunt, but AFAICT this proposal remains as vague as it's been before.
We really don't want to redefine all these things if we can avoid it. It would take a long time to understand the implications. Nor is it clear how all these redefinitions work. "And in doing so" is meaningless w/o actual explaining what it is that's being done in precise detail. There is also no need to "lay the ground work for a potential use of all interfaces outside of generic code": that possibility exists already. The existing definitions in the spec suffice, we simply have to remove the restrictions in place. Some comments:
Note that in the current Go spec, we use the term "constraint" simply to denote an interface in a "constraint position". It's still just an interface. A constraint is not a new thing. Also, in the current Go spec, we have types (well defined), type sets (which are also well defined, namely sets of these types, but excluding interface types), and that's it. We have a clear understanding of when two types are the same (identical is defined for types). We can therefore tell if a type is included in a type set. We can therefore tell if a type set if a subset of another type set. And so forth. All these things are pinned down in exact details. Otherwise we don't know how to implement this. The examples, which are the ones from #56548, arrive at the same result. There's no explanation as to why that is the case. But more importantly, if one arrives at the same result, what is the point of this proposal? It all sounds much more complicated. In summary, it is not clear to me why there is anything here that is not already achieved with the much simpler #56548. There's also no need to lay any groundwork for the generalized use of interfaces. We have laid that groundwork with the introduction of type sets. I believe this proposal should be declined. |
Sorry, I believe it's only vague if one only reads the proposal paragraph which just signify the scope of the change. Everything is explained in the later parts (and the abstract). How can I make this paragraph better? Also, let me address your specific comments to hopefully make things clearer:
I'm not proposing to add anything to Go that is not here already. While we arrive at the same result as #56548 I think it is objectively much easier to reason about: |
Just to be clear, I did read the entire rewritten section.
The rules in #56548 certainly can handle the Anyway, thanks for trying to clarify this proposal. Still, I don't think there's enough here to proceed in any meaningful way. I have tried to understand what you're saying and have failed. I will refrain from further comments. Thanks. |
Thank you for reviewing this. Just to clarify some more.
Thanks again. |
And in fact, for the first point, now that I think of it, I should probably have written I will make the amendments and try to think about how to deal with sets of sets of constraints in more details. Thanks again. |
@griesemer @ianlancetaylor here is the high level idea regarding checking interface implementation (as checking the presence of sets of constraints) in a bit more details. Hope you or anyone else can take a look whenever you have time and critique it. Thanks again. Currently, the constraints on types that can be represented by interfaces in Go are (afaict) :
Taking the abstract example of : type C interface{
a | b | c | d
e | f
g | h
} Implementing C is merely implementing either In theory, checking for C may seem exponential in the numbers of constraints being combined (o(kN), EXPSPACE in the WORST case only). However, in practice, the restriction on the domains of many constraints (which makes them incompatible with each other) simplify things. (e.g. if a is So it shouldn't be a big worry, I think. Generalized Interface implementation checkingThere are a few steps needed to check that a type
Tranforming a type constraint into a Tree Representation of a Boolean FormulaThe idea is that for a type One way to represent such constraints is to use a tree datastructure such as a Multi-valued Decision Diagram (MDD) that would represent the different constraint satisfaction branches of a boolean formula. The idea of checking the presence or absence of sets of constraints is basically just that same idea implemented as an adhoc MDD. // ConstraintNode is what the decision tree is made of.
// Each node accumulates the constraint of its ancestor nodes in a Constraint object.
type ConstraintNode{
Accumulator Constraint // representation of the overall set of constraints that need to be present when checking that node
Next []*ConstraintNode // holds the list of constraint added by conjunction (e.g. if the current node represents `a`, then Next hold `e` and `f` representations)
}
type Constraint struct{
// Kind can be a combination of:
// Type (binary: 00001)
// CoreType (00010)
// MethodSet (00100)
// Comparable (01000)
// OtherConstraint (10000)
// Invalid (00000)
Kind int
Type *types.Type
CoreType *types.Type
MethodSet map[types2.Signature]bool
Comparable bool
Other []*ConstraintNode // if a term of the union is another constraint that holds a union, we just store a ref to its MDD root
} Checking interface implementation
Reminder re. constraint satisfactionconstraint satisfaction is not constraint implementation. type sets ?We wouldn't need to compute them exactly anymore. The type set would be informally constructed by the types which satisfy the tree representation (MDD) of the constraint set. is it backtracking? Dynamic programming?Yes but it's not really a problem because once the MDD has been established and branches impossible to satisfy have been cleaned out, checking whether a type satisfies the corresponding constraint is merely checking whether it is compatible with one of the leaf nodes set of constraints. No need to retraverse the whole tree. |
I also think that it's very likely that we can convert constraint satisfaction into a boolean formula. But determine whether a boolean formula is satisfiable is itself an NP complete problem. You suggest that this shouldn't be a big worry, and I agree. But the type set approach isn't a big worry either in practice. It's only in edge cases that we run into problems, and edge cases will also be a problem with the boolean formula approach. |
Yes, I understand. Actually, if we can remain with the current implementation choices that's fine with me by all means.
without introducing special cases (simpler to explain) for now and later. It's a bit unfortunate that the easiest way to make these changes require a bit more implementation work, afaict. Then again, you probably are also aware of some edge cases that I'm not thinking about?
Thanks again :) |
Abstract
By reviewing a few concepts, we can define type sets, constraint interface satisfaction, interface implementation, in a way that makes the
comparable
interface obey a general rule instead of being an exception. In doing so, constraint interfaces would be strictly equivalent to interface types. This would future-proof their use as regular types.Basically, a Go type defines structure and behaviors i.e. (a set of constraints) on raw data.
Some set of constraints are first-class citizens in Go and are represented by interface types.
The types that satisfy a given set of constraints form a type set.
e.g.
any
is less constraining thancomparable
: it does not implement comparable because its type set holds types that do not satisfycomparable
int
implementscomparable
: every type in the type set ofint
satisfiescomparable
Now
comparable
can just simply be one of these interface types that describe a set of constraints: {"has the comparison operators == and !=} just as defined in the spec.All current Go interface types satisfy this constraint even though they rarely implement it. That's why they can instantiate type parameters constrained by
comparable
.comparable
could be used as a regular type in the future: assignment requiring interface implementation, it would ensure the absence of panic on equality testing for types satisfying it.Remains to model type sets in terms of sets of constraints.
While type sets are often infinite, we know that the constraints of the language are of finite quantity.
The rest is set operations on sets of sets of constraints (this is not a stutter, this is accounting for unions)
Proposal
(Re)define type set, interface satisfaction, interface implementation so that
comparable
follows a general rule of interface types. And in doing so, lay the ground work for a potential use of all interfaces outside of generic code ( interfaces as sum/union types).Background
What is an interface?
Types specify structural and behavioral constraints on raw data.
The Go language has a special construct in its type system that can be used to describe some of these constraints: interface types.
(note that, by virtue of being types themselves, these interface types also satisfy their own set of constraints that may be completely unrelated)
An interface type can describe constraints on types such as:
comparable
)An interface type may also describe a set of constraints as a combination of the above, using:
What is a type set?
Since a type specify a set of constraints that can be potentially satisfied by other types as well, we can gather these constraint-satisfying types into what is called a type set.
Type sets are fully described by a set of constraints. In mathematics, this would be set comprehension notation. A constraint would be a set intension i.e. a proposition.
As such, we do not need to compute type sets specifically (theoretically, a lot of type sets are infinite anyway).
We can just describe them using the finite number of constraints that can be modeled in Go.
I
:I
. By definition, that includesI
itselftype I = interface {~T}
the typeset ofI
is the set of types whose underlying type isT
I
is defined as a union of type terms, the type set of I is the union of the type set of each termT
:T
is a composite type (struct, array, slice, channel, ...), the type set of T includes all possible combinations obtained by substitution of the typeU
of an element with a restricted version ofU
where the type set is now a singleton composed of any one of the members of the initial type set ofU
. If T is not a composite of interface types, type set ofT
is therefore the singleton{T}
T
is not a composite type, then the type set ofT
is the the singleton{T}
It's interesting to note that for composites of interface types, the type set includes types that may have not been defined in a Go program.
In theory, these are subtypes. Since variance is restricted in Go, they do not necessarily appear as runtime types. This is not an issue since we do not propose to compute type sets explicitly but to rather operate on the set of constraints that define them.
Constraint satisfaction and interface implementation
While a type may satisfy a set of contraints, if that type is an interface type, that interface type may be used to describe a totally different set of constraints.
For example,
interface {int | string | bool}
satisfies the constraints shared by all interface types. But it describes a whole other set of constraints. Therefore, it does not satisfy the set of constraints it denotes.The aforementioned example shows that constraint satisfaction and interface implementation are different notions altogether, where interface implementation is taken to indicate constraint enforcement (or entailment).
If types may satisfy some constraints, they do not necessarily enforce them. (e.g.
any
satisfiescomparable
just as any interface currently, but a type satisfyingany
does not mean it satisfiescomparable
)Said otherwise, interface implementation tests that the constraints described by an interface can be found in the set of constraint defined by another type. This is a comparison of constraintive power.
A type
T
implements an interfaceI
typeset(T) ⊆ typeset(I)
constraint_set_described_by I ⊆ constraint_set_described_by T
i.e. every type that satisfies the constraints of T also satisfies I.
Interface implementation is constraint satisfaction over the type set of a type. It is not constraint satisfaction of the type itself.
As showed above, in general, interface implementation does not imply interface satisfaction and vice versa.
For example:
interface{int | string | bool}
implements itself but does not satisfy itself. (this is good, this is what we'd expect)interface{int | string}
implementsinterface{int | string | bool}
but does not satisfy it or itself (the interface is not in the type set)any
satisfiescomparable
but does not implement itWith the introduction of interface types and their semantics, there is a distinction between a type and its type set which is the reason for such difference.
type set of a type parameter
The type set of a type parameter is somewhat unknown. We just know that the type parameter itself satisfies a constraint interface.
In general, it can only be known to be assignable to itself.
It can be understood as a mostly opaque type variable but not necessarily an interface type.
Comparable type parameter
The spec states that: "In any comparison, the first operand must be assignable to the type of the second operand, or vice versa."
Assignability is based on interface implementation. The issue here is that a type parameter is not known to implement any specific interface more often than not.
Usually, we don't know a type parameter's type set before instantiation in the general case. We just know that every type implements itself.
The general rule is that a type parameter satisfies an interface if its constraint implements it.
Examples
Let's borrow the examples from #56548 (spoiler: we arrive at the same result)
To be comparable means to satisfy
comparable
which itself means to satisfy the set of constraint described bycomparable
.It does not mean to implement
comparable
, the latter implying some sort of substitutability (which appears in Go during value creation such asstruct{any}{int(2)}
or type assertions)If we want to be able to require from a type parameter that it implements
comparable
, we will need something else. In the meantime, there should be no difference between what can be expressed in generic Go vs regular Go.Observations
spec (AFAICT)
Unchanged:
Would need modifications:
Appendix
A similar treatment can be found in the type theory literature under the name semantic subtyping and perhaps CSP.
For most of the constraints expressed in Go, we would rarely (if ever) need to prove the satisfiability of interface types (which is just proving that they can be inhabited, i.e. proving that their type set is not empty).
Notably because the type checker forbids a lot of unsatisfiable interfaces already, and also because the expressivity around creating new interfaces is limited. Union interface changes that a little but not that much.
In any case, it should let us establish the semantics of an hypothetical FutureGo in one fell swoop, hopefully.
The text was updated successfully, but these errors were encountered: