Generics Draft: Mixing types and methods.

257 views
Skip to first unread message

Bruno Albuquerque

unread,
Aug 2, 2019, 4:12:42 PM8/2/19
to golang-nuts
I was thinking about a way to  "extend" usual operations (say, equality checks) to types that can not be compared the usual way (they are not "comparable" in the contract sense) and came up with something like this:

// Just to use for type assertion later.
type Equaler interface {
  Equal(Equaler) bool
}

contract Comparable(T) {
  T comparable(T), Equal(T) bool
}

func Compare(type T Comparable)(a, b T) bool {
  if eq, ok := a.(Equaler); ok {
    return eq.Equal(b)
  }

  return a == b  // Does this work at all?
}

Would this work? More specifically it looks to me that that if the specific type is not comparable (But has the Equal method), the compiler might see the "==" comparison in the function and give an error.

One way around this would possibly be to use something similar to type assertion (in this case, a and b would have to be asserted to "comparable" which I guess is not possible as it is a contract). Or, the compiler could be smart enough to know that if we reached that check, then the type must be comparable (so it would also not give an error).
 

Ian Lance Taylor

unread,
Aug 2, 2019, 4:28:47 PM8/2/19
to Bruno Albuquerque, golang-nuts
In the current design draft, that would not work. The == operator is
not supported by all possible types, so it is not permitted.

We definitely don't want to rely on the compiler being smart enough.
Any such approach would require writing down the exact inference rules
that the compiler is permitted to use. Otherwise different compilers
would behave differently.

One possibility we've toyed with is

switch T.(type) { // Note: switch on type parameter itself, not a
value of that type.
case Equaler:
...
case comparable:
// Permit using == operator here on values of type T in this case only.
}

We'll see whether some such facility seems useful. This is something
we can add later, if the current design draft seems workable.

Ian

Bruno Albuquerque

unread,
Aug 2, 2019, 4:40:57 PM8/2/19
to Ian Lance Taylor, golang-nuts
Ok, it makes sense. I do think that supporting something like this might make the proposal even more appealing as it will bring custom types somewhat closer to builtin types by allowing generic functions/methods that can act on both at the same time.

Axel Wagner

unread,
Aug 2, 2019, 6:55:48 PM8/2/19
to Bruno Albuquerque, Ian Lance Taylor, golang-nuts
FWIW:
interface{}(a) == interface{}(b)
would work. It would panic if a and b have the same, non-comparable type. But if you know the type is equal and comparable, it's well-defined and does what you want. So you have to modify your code a bit:

type equaler(type T) interface {
  Equal(T) bool
}

contract Comparable(T) {
  T comparable, Equal(T) bool

}

func Compare(type T Comparable)(a, b T) bool {
  if eq, ok := a.(equaler(T)); ok {
    return eq.Equal(b)
  }
  // Okay, this is weird, but: If you have `func (*T) Equal(T) bool`, a T (value) would
  // be accepted by the contract, as contracts don't distinguish between value and
  // pointer-receivers. But it would fail above type-assertion, as values don't include
  // pointer-methods in their method set.
  if eq, ok := (&a).(equaler(T)); ok {
    return eq.Equal(b)
  }
  return interface{}(a) == interface{}(b)
}

I can't think of anything in the current design draft preventing this from working (though I'm sure Ian can correct me if I'm wrong).

It's a special case for equality-comparison though, it doesn't generalize to any other operators.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAEd86Tz4tvx3qPZ4fV86AyV-B8YOXEMZMR-L4f6fcTyAuX_eXQ%40mail.gmail.com.

Axel Wagner

unread,
Aug 2, 2019, 7:03:46 PM8/2/19
to Bruno Albuquerque, Ian Lance Taylor, golang-nuts
Just tried running it through the prototype type-checker and one correction: You also have to convert `a` and `&a` to interface{}, so that you are allowed to type-assert on them:
  if e, ok := (interface{}(a)).(equaler(T)); ok { return e.Eq(b) }
  if e, ok := (interface{}(&a)).(equaler(T)); ok { return e.Eq(b) }

Anyway, I hope by now the extreme hackishness of this "solution" is clear :)

Robert Engels

unread,
Aug 2, 2019, 7:54:53 PM8/2/19
to Axel Wagner, Bruno Albuquerque, Ian Lance Taylor, golang-nuts

Steven Blenkinsop

unread,
Aug 3, 2019, 12:26:15 AM8/3/19
to Axel Wagner, Bruno Albuquerque, Ian Lance Taylor, golang-nuts
On Fri, Aug 2, 2019 at 6:55 PM 'Axel Wagner' via golang-nuts <golan...@googlegroups.com> wrote:

contract Comparable(T) {
  T comparable, Equal(T) bool
}

Wait, does this work? I mean, `comparable` is a contract, but it's being used as a constraint. Could you also write:

contract Equaler(T) {
    T Equal(T) bool
}

contract Comparable(T) {
    T comparable, Equaler
}

? Or is comparable just special? Or does this not actually work, even for comparable? This would certainly be convenient if you could do it, though it means contracts involving a single type are privileged over others (you can create a disjunction between constraints, but not between contracts, for example). It seems like if you can create contract constraints like this, you'd also want to be able to make generic contract constraints, to capture things like

contract Appender(type Elem)(T) {
    T Append(...Elem) T
}

contract Appendable(type Elem)(T) {
    T []Elem, (Appender(Elem)) // Parens to avoid parsing like a method constraint
}

The same issue comes up with the

    switch T.(type) { ... }

idea. Even if you're only constraining one type at a time like this, you might still need to express a relationship to another type parameter. Unless this is allowed:

    switch (type) {
        case comparable(T): ...
        case Equaler(T): ...
    }

Ian Lance Taylor

unread,
Aug 3, 2019, 2:12:04 AM8/3/19
to Steven Blenkinsop, Axel Wagner, Bruno Albuquerque, golang-nuts
On Fri, Aug 2, 2019 at 9:25 PM Steven Blenkinsop <stev...@gmail.com> wrote:
>
> On Fri, Aug 2, 2019 at 6:55 PM 'Axel Wagner' via golang-nuts <golan...@googlegroups.com> wrote:
>>
>>
>> contract Comparable(T) {
>> T comparable, Equal(T) bool
>> }
>
>
> Wait, does this work? I mean, `comparable` is a contract, but it's being used as a constraint.

You're right, I wrote it wrong. Actually I'm not sure how to write
that contract. Ideally we want to say that T is either comparable or
has the Equal(T) bool method, but there is no way to write that. We
can embed comparable(T), but we can't do that in a disjunction with
Equal(T) bool.


> The same issue comes up with the
>
> switch T.(type) { ... }
>
> idea. Even if you're only constraining one type at a time like this, you might still need to express a relationship to another type parameter. Unless this is allowed:
>
> switch (type) {
> case comparable(T): ...
> case Equaler(T): ...
> }

I suspect that switch T.(type) would have to only permit disjunctions
listed in the contract for T.

Ian

Steven Blenkinsop

unread,
Aug 3, 2019, 11:10:30 AM8/3/19
to Ian Lance Taylor, Axel Wagner, Bruno Albuquerque, golang-nuts
On Sat, Aug 3, 2019 at 2:11 AM, Ian Lance Taylor <ia...@golang.org> wrote:
You're right, I wrote it wrong.  Actually I'm not sure how to write that contract.  Ideally we want to say that T is either comparable or has the Equal(T) bool method, but there is no way to write that.  We can embed comparable(T), but we can't do that in a disjunction with Equal(T) bool.

So I guess I was building castles in the sky, then. Though, just allowing this does seem like a viable option. I would guess that the reason not to allow disjunction between contracts or between constraints on different types is so that whether each type satisfies a contract can be decided without reference to the constraints on any other type. Allowing disjunction between single-parameter contracts applied to the same type would preserve this property, and the syntax of using these contracts as constraints would make the restriction that they have to be applied to the same type fall naturally out of the syntax. You lose the property that the constraints for any particular type parameter are already in conjunctive normal form, but you already need to do normalization to check the conjunctive normal form against the disjunctive normal form in order to determine whether one contract allows its parameter to satisfy another contract (when calling generic code from generic code).

As for the generic contracts I was showing, they would be very useful if this were allowed, but I can understand the teachability hazard of having two kinds of type parameters on a single declaration. The draft already has two kinds of type parameters, since contract parameters are different from regular type parameters, but as long as you never have both kinds of parameters in play, people don't necessarily need to understand the distinction between them.
Reply all
Reply to author
Forward
0 new messages