generics draft and specialization

268 views
Skip to first unread message

Jon Reiter

unread,
Jun 21, 2020, 1:49:05 AM6/21/20
to golang-nuts
the draft lays out an example function GeneralAbsDifference.  these comments build around that.  i wanted to see how far i could get towards working specialization within this framework.  the baseline assumption is that when specialization is required today the acceptable amount of type switching/assertion goes up a bit.

so the simplest variation is
func GeneralAbsDifference(a,b interface{}) interface{} {
 ai, aiok := a.(int)
 bi, biok := b.(int)
 if aiok && biok {
   return OrderedAbsDifference(ai, bi)
 }
 // repeat for all types
}

that works but is, even given the qualifier above, not really acceptable.  you can make it a bit cleaner with reflect but still, it's bad.  changing interface{} to Numeric doesn't work with compile error "interface type for variable cannot contain type constraints".  fair enough, although i'm not convinced no such useful check is possible at compile time.  with that facility available it's debatable whether some of those otherwise-not-so-clean setups are worth it in exchange for only having to write the underlying absdiff() functions once each.

if we pretend the arguments can be of type Numeric we can try our type logic as:
 ao, aook := a.(OrderedNumeric)
 ...
in hopes of getting a somewhat-cleaner arrangement.  this, with a and b using interface{} so it could otherwise compile, returns the entertaining error message "OrderedNumeric does not satisfy OrderedNumeric."  and it is true that type does not appear in the type list of the interface.  in this case there are two arguments and we'd need to ensure they are of the same acceptable type.  i was expecting this to compile and then to write some logic using reflect and see how badly it turned out.  that's not an option.

but consider this for a single-argument function.  then there is no uncertainty -- the logic i'd need to write is straightforward and can surely be automatically generated.  i think that is true whenever each type parameter specifies only 1 argument's type.  multiple-argument generic-specialization would then require creating "tuple interfaces" that glue the params together but would be otherwise clean and explicit.  it's certainly better than what you'd need to do today to handle any of this.

so two specific questions:
 - is there some part-way use of types in interfaces that can be used in non-generic function arg specs?  in this case the return value would require it but in general arguments-only would be useful too.
 - at least for cases where no ambiguity exists is it possible for a type-containing interface to act like it's on it's own type list?

some type logic is always going to be necessary given the language setup.   i just want to avoid having to write purely-mechanical if-else blocks to appease the compiler here, or using interface{} and throwing out compiler assistance.

this feels adjacent to the discussion on "identifying the matched predeclared type" but my comment is already too long so i'll stop there.


Ian Lance Taylor

unread,
Jun 22, 2020, 12:00:43 AM6/22/20
to Jon Reiter, golang-nuts
On Sat, Jun 20, 2020 at 10:48 PM Jon Reiter <jonr...@gmail.com> wrote:
>
> the draft lays out an example function GeneralAbsDifference. these comments build around that. i wanted to see how far i could get towards working specialization within this framework. the baseline assumption is that when specialization is required today the acceptable amount of type switching/assertion goes up a bit.
>
> so the simplest variation is
> func GeneralAbsDifference(a,b interface{}) interface{} {
> ai, aiok := a.(int)
> bi, biok := b.(int)
> if aiok && biok {
> return OrderedAbsDifference(ai, bi)
> }
> // repeat for all types
> }
>
> that works but is, even given the qualifier above, not really acceptable. you can make it a bit cleaner with reflect but still, it's bad. changing interface{} to Numeric doesn't work with compile error "interface type for variable cannot contain type constraints". fair enough, although i'm not convinced no such useful check is possible at compile time. with that facility available it's debatable whether some of those otherwise-not-so-clean setups are worth it in exchange for only having to write the underlying absdiff() functions once each.
>
> if we pretend the arguments can be of type Numeric we can try our type logic as:
> ao, aook := a.(OrderedNumeric)
> ...
> in hopes of getting a somewhat-cleaner arrangement. this, with a and b using interface{} so it could otherwise compile, returns the entertaining error message "OrderedNumeric does not satisfy OrderedNumeric." and it is true that type does not appear in the type list of the interface. in this case there are two arguments and we'd need to ensure they are of the same acceptable type. i was expecting this to compile and then to write some logic using reflect and see how badly it turned out. that's not an option.
>
> but consider this for a single-argument function. then there is no uncertainty -- the logic i'd need to write is straightforward and can surely be automatically generated. i think that is true whenever each type parameter specifies only 1 argument's type. multiple-argument generic-specialization would then require creating "tuple interfaces" that glue the params together but would be otherwise clean and explicit. it's certainly better than what you'd need to do today to handle any of this.
>
> so two specific questions:
> - is there some part-way use of types in interfaces that can be used in non-generic function arg specs? in this case the return value would require it but in general arguments-only would be useful too.

Possibly such a thing could be defined, but it's not in the current
design draft. And I'm not sure it would help. If I write F(a, b
Ordered) then both a and b may be Ordered, but nothing requires that
they be the same ordered type.

> - at least for cases where no ambiguity exists is it possible for a type-containing interface to act like it's on it's own type list?

I guess I'm not sure why you need this. If you have a type list, it
seems feasible to type switch on each possible type. I'm sure I'm
missing something.

Ian

Jon Reiter

unread,
Jun 22, 2020, 12:41:07 AM6/22/20
to Ian Lance Taylor, golang-nuts
yeah agree i only see a simple way for 1 argument (maybe i did not express that clearly).  you could rewrite multiple arguments using generic lists i guess, or explicit Pair, Triplet, etc types.
 

>  - at least for cases where no ambiguity exists is it possible for a type-containing interface to act like it's on it's own type list?

I guess I'm not sure why you need this.  If you have a type list, it
seems feasible to type switch on each possible type.  I'm sure I'm
missing something.

so the code looks like:
if x1,ok1 := a.(first) ; ok {
} else if x2, ok2 := a.(second) {
...
} else {
 // never get here as long as every type is covered
}

but that "never get here" is only true as long as the if/elses match every type in the type list.  you could get halfway with something like:
for t := range reflect.TypesOnTheList(a) { // maybe reflect.TypesOnTheList(reflect.GenericTypeOf(a))
 if x,ok := a.(t) ; ok {
  GenericFunction(x)
 }
}

this is a step more than syntactic sugar wrt the spec as it automates some maintenance.  with that function in reflect it's just convenience.  the reflect version here is similar to your "reflection on type arguments" section and could be done that way too.  here i think you can get the compiler to reject GenericTypeOf() on a non-generic type and ensure the loop always hits it's mark.

 

Ian

Jon Reiter

unread,
Jun 22, 2020, 12:52:07 AM6/22/20
to Ian Lance Taylor, golang-nuts
sorry, previous is v unclear as it's assuming a solution to question 1 in clarification on question 2.  amended below...

  Generic1(x1) 
} else if x2, ok2 := a.(second) {
      Generic2(x2) 
...
} else {
 // never get here as long as every type is covered
}

but that "never get here" is only true as long as the if/elses match every type in the type list.  you could get halfway with something like: 
for t := range reflect.TypesOnTheList(a) { // maybe reflect.TypesOnTheList(reflect.GenericTypeOf(a))
  if t == A || t == B || ... { 
 if x,ok := a.(t) ; ok {
  GenericFunction(x)
 }
} else if t == C || t== D || ... {
    if x,ok := a.(t) ; ok {
  GenericFunction2(x)
 }
}

this is a step more than syntactic sugar wrt the spec as it automates some maintenance.  with that function in reflect it's just convenience.  the reflect version here is similar to your "reflection on type arguments" section and could be done that way too.  here i think you can get the compiler to reject GenericTypeOf() on a non-generic type and ensure the loop always hits it's mark.

punting on question 1, you can do this with something like what is proposed in the reflection on type arguments section, i think quite safely.
 

 

Ian
Reply all
Reply to author
Forward
0 new messages