Generics: after type lists

813 views
Skip to first unread message

Patrick Smith

unread,
Aug 7, 2020, 7:33:59 PM8/7/20
to golang-nuts
I like the second draft for generics. It seems to me a large
simplification and improvement over the first draft. Considering just
the state of Go today, I would be quite happy with this, even if it's
not perfect. Thanks to Ian, Robert, and everyone else for their work
on this.

Also, I would vote for square brackets over parentheses.

But I do have concerns related to the future development of Go. In
particular, if we think it likely that a future version of Go will
allow operator overloading, then perhaps type lists are not the best
choice.

To my mind, the biggest defect in the design draft is that we can't
write generic functions and types that work transparently with both
builtin and user-defined types (that do not inherit appropriate
behavior from an underlying builtin type). For example, we can't write
a

func Min[type T ...](a, b T) T { ... }

that works both when T is int and when T is

type BigInt struct { i *big.Int }

Instead, we would use workarounds such as writing two versions of Min,
or passing in an adaptor function or object; in the case of Min, a
comparison function. And that's OK, especially in an initial version
of generics.

But generics would be significantly easier to use if we could write
functions that work on both builtin and user-defined types. The two
most likely candidates for allowing this seem to be operator
overloading (where BigInt might have a method named "<", "operator<",
or some such, that allows it to be used with the < operator) and
methods on builtin types (where int might be given a method named Less
with the same behavior as the < operator). Of course, other solutions
could be imagined, but I'll confine my speculations to those two.

Now let's try to imagine how sorting slices might be implemented in
the standard library in various futures. Of course, the current sort
package would have to be kept and maintained for a long time.

If Go2 implements the current draft with type lists, then we might add
a sort2 package containing something to:

func SliceBy[type T](s []T, less(T, T) bool) { ... }

type Ordered interface { // Copied from the draft
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
string
}

func Slice(type T Ordered)(s []T) {
SliceBy(s, func(a, b T) bool { return a < b })
}

type Lesser[type T] interface { Less(T) bool }

func SliceByLess(type T Lesser[T])(s []T) {
SliceBy(s, T.Less)
}

All well and good. Now say time goes by and Go3 adds operator methods.
Nothing in the sort2 package expresses a unified sort using operator
methods, so we need a new package sort3:

func SliceBy[type T](s []T, less(T, T) bool) { ... }

type Lesser[type T] interface { <(T) bool } // Or whatever the syntax is.

func Slice(type T Lesser)(s []T) {
SliceBy(s, func(a, b T) bool { return a < b })
}

(We might just add sort3.Lesser and sort3.Slice into the sort2 package
under different names, but I suspect the aim would be to eventually
deprecate sort2.)

The effects will ripple through other code, both in and outside the
standard library. Suppose some Go2 code has a chain of generic
functions A calls B calls C calls D, where each exists in two
versions, one for builtin types and one for user-defined types, and
the two versions of D call sort2.Slice or sort2.SliceByLess. When Go3
with operator methods arrives, if we want to unify these, we have to
write a third version of each of A, B, C, and D, where D calls
sort3.Slice.

On the other hand, suppose Go2 has type lists and Go3 gives builtin
types methods corresponding to operators. Assuming the name Less is
used for <, sort2.SliceByLess now handles both builtin and
user-defined types, so we don't need a sort3 package. And in the ABCD
scenario, we can just keep the SliceByLess version of each, and
quietly let the sort2.Slice versions vanish as they become unused.

[Important point===>] This means that if Go2 has type lists in
interfaces, there will be a strong incentive for Go3 to give builtin
types methods, even if we think that operator overloading is otherwise
a superior solution, because operator overloading will require much
more new code to be written.

Instead of using type lists, suppose Go2 allowed interfaces to require
the presence of specific operators. Then the sort2 header might look
like this:

func SliceBy[type T](s []T, less(T, T) bool) { ... }

type Lesser[type T] interface { <(T) bool }

func Slice(type T Lesser)(s []T) {
SliceBy(s, func(a, b T) bool { return a < b })
}

type MLesser[type T] interface { Less(T) bool }

func SliceM(type T MLesser[T])(s []T) {
SliceBy(s, T.Less)
}

(Note that the first part is identical to the previous sort3 header.
But in Go2 we also need MLesser and SliceM in order to handle
user-defined types.)

This leaves us more easy options in Go3. If Go3 implements operator
overloading, then sort2.Slice now handles both builtin and
user-defined types, and code using sort2.SliceM can be allowed to
wither away. If Go3 implements an int.Less method, then sort2.SliceM
is now the good version, and code using sort2.Slice can be allowed to
wither away as it becomes unused.

So maybe the alternative of allowing interfaces to require specific
operators deserves another look, to see if it's really not viable in
Go2. I would suggest that perhaps the initial version of generics need
not support all operators; maybe it's enough to support only those
that apply to numeric types (string should be accepted by interfaces
requiring +, <, and other comparison operators). Or maybe a slightly
larger subset of operators would do.

As a final note - this post, like all speculations about the future,
is rather fuzzy. I realize that. Nevertheless, I think it is important
to realize that the choices we make now carry consequences for our
options after a few years' time.

Kent Sandvik

unread,
Aug 7, 2020, 8:33:05 PM8/7/20
to Patrick Smith, golang-nuts
Operator overloading will never -- hopefully -- be implemented. It's a perfect way to obscurate code. --Kent


--
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/CAADvV_stjEvD4pLwGNmahAEzc0UDNLuzwttZ9wDg78BOJfDA5Q%40mail.gmail.com.

Carla Pfaff

unread,
Aug 7, 2020, 11:58:17 PM8/7/20
to golang-nuts
On Saturday, 8 August 2020 at 01:33:59 UTC+2 Patrick Smith wrote:
if we think it likely that a future version of Go will allow operator overloading

That's highly unlikely: https://golang.org/doc/faq#overloading 

Axel Wagner

unread,
Aug 8, 2020, 5:02:04 AM8/8/20
to Carla Pfaff, golang-nuts
That seems like a very strong reading of "it seems more a convenience than an absolute requirement. Again, things are simpler without it". In particular, generics change the equation for its usefulness (as discussed in this thread).

I'm not saying Go *will* get operator overloading or even that it *should* get it, but I don't think that FAQ-section is an argument that it *won't*. 

--
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.

Viktor Kojouharov

unread,
Aug 9, 2020, 1:26:51 PM8/9/20
to golang-nuts
There's a big difference between creating arbitrary operators like c++, and just allowing the predefined set of operators to be implemented for custom types only. The later, combined with the overall aversion of the Go community towards operator overloading as a whole, will probably lead to very conservative use overall, with the added benefits listed by OP, and egronomic benefits to using certain mathematical types in Go, such as matrices.

Ian Lance Taylor

unread,
Aug 9, 2020, 9:29:53 PM8/9/20
to Patrick Smith, golang-nuts
On Fri, Aug 7, 2020 at 4:33 PM Patrick Smith <pat42...@gmail.com> wrote:
>
Thanks for the detailed comment.

I think the key statement in your argument is this one:

> But generics would be significantly easier to use if we could write
> functions that work on both builtin and user-defined types. The two
> most likely candidates for allowing this seem to be operator
> overloading (where BigInt might have a method named "<", "operator<",
> or some such, that allows it to be used with the < operator) and
> methods on builtin types (where int might be given a method named Less
> with the same behavior as the < operator). Of course, other solutions
> could be imagined, but I'll confine my speculations to those two.

I think this is open to question. In C++, for example, std::sort
takes an optional comparison class. In effect, the default if no
comparison class is provided is to use operator<. That is a
reasonable and appropriate choice for C++. But Go does not have
function overloading and does not have default values for arguments
(https://golang.org/doc/faq#overloading). So the natural way to write
a sort function is to provide a comparison function. That is how
sort.Slice works, for example. sort.Sort works differently because it
needs three different functions, so they are passed in via a type
rather than as an argument. In typical use, people will convert to
that type when they call sort.Sort, so they are providing the required
functions via a type conversion.

If we accept this argument, then in Go it wouldn't be appropriate to
write a single function that works on both builtin and user-defined
types. Writing such a function would be relying on some sort of
default comparison function, which is not the typical Go approach.

So instead we need to ensure that it is very easy to pass a comparison
function, regardless of whether you are using a builtin type or a
user-defined type. And I think that that is already true.

Ian

Richard Oudkerk

unread,
Aug 10, 2020, 12:46:36 PM8/10/20
to golang-nuts
Another way to bridge the gap between builtin and custom types could be to have a package op that has functions that delegate to either an operator or a method.  Then you could write generic functions like

func Min[type T op.Lessable](a, b T) T {
  if op.Less(a, b) {
    return b
  }
  return a
}

For each function Foo in op, op.Foo(a, ...) would delegate to either a.Foo(...) or to a builtin operator, and there would be an associated interface op.Fooable. For example
  • op.Add(a, b) is equivalent to a.Add(b) or a + b
  • op.Len(a) is equivalent to a.Len() or len(a)
  • op.Get(a, i) is equivalent to a.Get(i) or a[i]
  • op.Range(a, f) is equivalent to a.Range(f) or for k, v := range a { f(k, v) }
I don't think op.Lessable is expressible with the latest proposal though.

burak serdar

unread,
Aug 10, 2020, 4:53:17 PM8/10/20
to Richard Oudkerk, golang-nuts
On Mon, Aug 10, 2020 at 10:46 AM 'Richard Oudkerk' via golang-nuts
<golan...@googlegroups.com> wrote:
>
> Another way to bridge the gap between builtin and custom types could be to have a package op that has functions that delegate to either an operator or a method. Then you could write generic functions like
>
> func Min[type T op.Lessable](a, b T) T {
> if op.Less(a, b) {
> return b
> }
> return a
> }


Would it be possible to make it explicit instead of trying to combine
builtin types and others?

type Number interface {
type int, int8, int16, int32, int64, unit, int8, int16, int32,
uint64, float32, float64
}

func Min(type T Number)(a, b, T) {
if a<b {
return a
}
return b
}

type Lessable interface {
func LessThan(interface{}) bool
}

func Min(type T Lessable(T))(a, b T) {
if a.LessThan(b) {
return a
}
return b
}

This would be similar to c++ template specialization. Specializations
could be limited to built-in types to limit ambiguity.
> --
> 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/f60e0dc5-60ea-4c82-9309-d55fb1b9b3adn%40googlegroups.com.

Patrick Smith

unread,
Aug 10, 2020, 6:52:33 PM8/10/20
to Richard Oudkerk, golang-nuts
On Mon, Aug 10, 2020 at 9:46 AM 'Richard Oudkerk' via golang-nuts
<golan...@googlegroups.com> wrote:
> Another way to bridge the gap between builtin and custom types could be to have a package op that has functions that delegate to either an operator or a method. Then you could write generic functions like
>
> func Min[type T op.Lessable](a, b T) T {
> if op.Less(a, b) {
> return b
> }
> return a
> }
...

Indeed, and quite a while ago I sketched out a proposal along those
lines: https://gist.github.com/pat42smith/ed63aca983d4ba14fdfa320296211f40
. There was very little
reaction to that proposal.

> I don't think op.Lessable is expressible with the latest proposal though.

Also true, so this almost certainly won't fly.

Patrick Smith

unread,
Aug 10, 2020, 7:07:44 PM8/10/20
to burak serdar, Richard Oudkerk, golang-nuts
On Mon, Aug 10, 2020 at 1:53 PM burak serdar <bse...@computer.org> wrote:
> Would it be possible to make it explicit instead of trying to combine
> builtin types and others?
>
> type Number interface {
> type int, int8, int16, int32, int64, unit, int8, int16, int32,
> uint64, float32, float64
> }
>
> func Min(type T Number)(a, b, T) {
...
> }
>
> type Lessable interface {
> func LessThan(interface{}) bool
> }
>
> func Min(type T Lessable(T))(a, b T) {
...
> }
>
> This would be similar to c++ template specialization. Specializations
> could be limited to built-in types to limit ambiguity.
>
> }

There are two problems with this. First, it would require either a
concept of "this or that" in interfaces, or duplicating all code that
calls Min, however indirectly. This or that:

type NumberOrLessable interface {
// Satisfied by any type that satisfies either Number or Lessable
Number | Lessable
}

func [type T NumberOrLessable](a, b, c T) T {
return Min(a, Min(b, c))
}

Or code duplication based on the interface that T satisfies:

func [type T Number](a, b, c T) T {
return Min(a, Min(b, c))
}

func [type T Lessable](a, b, c T) T {
return Min(a, Min(b, c))
}

Second, Ian has long indicated strong opposition to specialization, as
I think have others.

Patrick Smith

unread,
Aug 11, 2020, 7:19:40 PM8/11/20
to Ian Lance Taylor, golang-nuts
Ian, thanks for taking the time to read and respond to my post.

On Sun, Aug 9, 2020 at 6:29 PM Ian Lance Taylor <ia...@golang.org> wrote:
> If we accept this argument, then in Go it wouldn't be appropriate to
> write a single function that works on both builtin and user-defined
> types.

This I disagree with; what you regard as inappropriate, I see as a
desirable goal. The question is whether there is a reasonable way to
achieve it, one for which the benefits outweigh the costs.

Before I explain why I disagree, let me mention that sorting was
perhaps not the best example for me to have used. It is very common to
sort things in an order different than the default (e.g. integers in
reverse order), or to sort things that don't have a default order
(e.g. countries by area, population, or date of formation). For this
reason, sort functions in any language very often accept a comparison
function.

A better example might be a function that finds the roots of a
polynomial with real-valued coefficients, where the coefficients are
taken from a type F that might be float32, float64, or a user-defined
type such as one based on big.Float. In this function, we want to use
the default arithmetic operations provided by F. It would be very
unusual to override those with a different choice of operations.

But in the type lists approach, unless we write two distinct versions
of the function, we must use an adaptor type or object (or multiple
adaptor functions) to indicate to the function what arithmetic
operations to use. There is no way for the function to access these
operations otherwise (except reflection).

> I think this is open to question. In C++, for example, std::sort
> takes an optional comparison class. In effect, the default if no
> comparison class is provided is to use operator<. That is a
> reasonable and appropriate choice for C++. But Go does not have
> function overloading and does not have default values for arguments
> (https://golang.org/doc/faq#overloading). So the natural way to write
> a sort function is to provide a comparison function.

That Go does not have default argument values is, I think, merely a distraction.
This is easily handled with multiple functions; one would like to write

func SortBy[type T](s []T, less func(T, T) bool) { ... }

func Sort[type T ...](s []T) {
SortBy(s, the_natural_order_on_T)
}

where the constraint on T in Sort expresses that T must have a natural ordering.

> If we accept this argument, then in Go it wouldn't be appropriate to
> write a single function that works on both builtin and user-defined
> types. Writing such a function would be relying on some sort of
> default comparison function, which is not the typical Go approach.

But Go does have a default comparison for builtin types - the <
operator. This is used by sort.Ints, sort.Float64s, and sort.Strings.
With generics and type lists, it will be possible to combine these
into a single function that also works on other builtin types.

One can also give a user-defined type a default comparison, by
supplying a Less method (or Cmp, as in big.Int). The difficulty is
that without generics, there is no way to write a function
specification requiring the presence of such a method.

Generics with type lists simplify both of these cases, but do not give
us a way to unify them.

Another example - the String method of a type describes the default
way of displaying the type's values in text. For types with no String
method, there is a default builtin way.

And displaying values as text is so very useful that Go already
provides a family of functions to do this, and that work on both types
with a String method and types without String. This is the fmt
package, and it uses reflection internally to achieve this.

Try to imagine writing printing code if fmt.Print were replaced by
these two functions:

// Prints values in a fixed builtin way, ignoring any String method
that might exist.
func PrintB(a... interface{}) { ... }

type Stringer interface { String() string }

func PrintS(a... Stringer) { ... }

Or if fmt.Print became:

func Print[type T](func tostring(T) string, a ...T)

I think you will agree that it would be quite a bit more awkward than
with the existing fmt.Print.

> If we accept this argument, then in Go it wouldn't be appropriate to
> write a single function that works on both builtin and user-defined
> types.

And I must reply - not only is it appropriate, it is already done in
one of the most widely used packages in the standard library.

The question is, is there a reasonable way to make this easier in other cases?

Ian Lance Taylor

unread,
Aug 11, 2020, 11:32:15 PM8/11/20
to Patrick Smith, golang-nuts
Thanks, it's an interesting example. big.Float isn't really the same
as float32 or float64. For example, big.Float doesn't provide any way
to write "a = b + c + d". You can only write, in effect, "a = b + c;
a += d". And big.Float doesn't have == != < <= >= >. Instead it has
expressions like "a.Cmp(b) == 0". So if you want to write a generic
function that works on float32/float64/big.Float, you're going to need
adaptors no matter what. You can't avoid them.

So in practice I don't think we're going to write the root-finding
function using parametric polymorphism at all. I think we're going to
write it using an interface type. And I think we're going to base
that interface type on big.Float. Then we need to write an
implementation of that interface for float32/float64. And that
implementation is likely to use parametric polymorphism so that we
only have to write it once to handle both float32 and float64. And
for that implementation, type lists work fine.


What you're looking for here, I think, is a case where there is a type
A that implements methods that are isomorphic to the basic arithmetic
and/or comparison operators. And then you want an algorithm that can
be used with that type, as well as with the builtin types. For such a
case, it might be nice to be able to write that algorithm only once.

And, of course, with the current design draft, we can write the
algorithm only once, provided we write it using methods. We will use
the methods defined by the type A. And we will need an adapter for
builtin types to implement those methods. And that adaptor can be
written using type lists.

Now, I think your original argument is that in the future we might
decide to introduce operator methods. And we might add those operator
methods to A. And then we can write our algorithm using operators.
But we might have a chain of functions already built using (regular)
methods, and we won't be able to use those with operator methods, so
we will be discouraged from using operator methods.

So first let me observe that that seems to me like a pretty long chain
of hypotheticals. Is there any type out there today that has methods
that correspond exactly to operators? There is a reason that
big.Float and friends don't have such methods: for any type that uses
pointers, they are inefficient. Is this possibility going to be a
real problem, or will it always be a hypothetical one?

Second, let me observe that in this example it seems to me that we
will already have the adapter types that add methods to the builtin
types. So the question would be: if we add operator methods, can we
very easily shift those adaptor types from using type lists to using
operator methods instead? And it seems to me that the answer is yes.
If we have operator methods, we will be able to write interface types
with operator methods. And if we do that it would be very natural to
let the builtin types implement those interfaces. And, of course, we
can use those interface types as constraints. So we just have to
change the constraints that our adapter types use from type lists to
interface types with operator methods. And everything else will work.
So, sure, operator methods might lead to some code tweaks. But they
won't require a massive overhaul.

Or so it seems to me. What am I missing?


> > I think this is open to question. In C++, for example, std::sort
> > takes an optional comparison class. In effect, the default if no
> > comparison class is provided is to use operator<. That is a
> > reasonable and appropriate choice for C++. But Go does not have
> > function overloading and does not have default values for arguments
> > (https://golang.org/doc/faq#overloading). So the natural way to write
> > a sort function is to provide a comparison function.
>
> That Go does not have default argument values is, I think, merely a distraction.
> This is easily handled with multiple functions; one would like to write
>
> func SortBy[type T](s []T, less func(T, T) bool) { ... }
>
> func Sort[type T ...](s []T) {
> SortBy(s, the_natural_order_on_T)
> }
>
> where the constraint on T in Sort expresses that T must have a natural ordering.

Yes. One way to state my point is to note that you had to give the
functions different names: Sort and SortBy. In C++ those two
functions have the same name, where your Sort function is in effect
SortBy with a default argument.


To me the point of your String example is: Go already has various ways
of writing generic code. You can use interfaces, methods, and type
assertions. You can use reflection. The generics design draft adds
another way: parametric polymorphism.

The particular topic here is whether adopting type lists today is
going to tie our hands in the future in a way that we don't anticipate
and that would be bad. It's possible of course, but it's not yet
clear to me that that must be so.

Thanks for the note.

Ian

robert engels

unread,
Aug 11, 2020, 11:48:22 PM8/11/20
to Ian Lance Taylor, Patrick Smith, golang-nuts
Isn’t the example with big.Float only a problem because of the pointer based design - which is much harder to write code with than a immutable value based one.

Then you have

D = A.Add(B).Add(C)

with

(Number n) func Add(a Number) Number

Which is straightforward, and the compiler creates the temp storage on the stack for intermediate results.

To perform similar math using pointer based methods you have to use ‘accumulator’ math, and most likely lots of temp variables - and the resulting number of allocations on the stack is probably the same.
> --
> 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/CAOyqgcWAieOXNDQFZ2YB3xzMqv19duh-9PzSZLpnd9M4AZCbwg%40mail.gmail.com.

Ian Lance Taylor

unread,
Aug 12, 2020, 12:06:02 AM8/12/20
to robert engels, Patrick Smith, golang-nuts
On Tue, Aug 11, 2020 at 8:47 PM robert engels <ren...@ix.netcom.com> wrote:
>
> Isn’t the example with big.Float only a problem because of the pointer based design - which is much harder to write code with than a immutable value based one.
>
> Then you have
>
> D = A.Add(B).Add(C)
>
> with
>
> (Number n) func Add(a Number) Number
>
> Which is straightforward, and the compiler creates the temp storage on the stack for intermediate results.
>
> To perform similar math using pointer based methods you have to use ‘accumulator’ math, and most likely lots of temp variables - and the resulting number of allocations on the stack is probably the same.

Sure, but my point is that there is a chain of hypotheticals here.
You are describing a possible type that as far as I know does not
exist in the wild. I think that the overall argument that Patrick is
presenting only holds if at least one such type does exist, and if
that type is used widely enough that it's worth writing algorithms
that work both for that type and for builtin types. It's definitely
possible. But as far as I know at present it's an entirely
hypothetical situation. That doesn't mean we shouldn't consider it.
But it does mean that we should also consider how likely it is to
arise.

Ian

Patrick Smith

unread,
Aug 14, 2020, 1:56:53 AM8/14/20
to Ian Lance Taylor, golang-nuts
On Tue, Aug 11, 2020 at 8:31 PM Ian Lance Taylor <ia...@golang.org> wrote:
> To me the point of your String example is: Go already has various ways
> of writing generic code. You can use interfaces, methods, and type
> assertions. You can use reflection. The generics design draft adds
> another way: parametric polymorphism.

The other point is that writing functions or types that work with both
builtin and user-defined types is not just good for reducing
duplication in the implementation. It also simplifies the use of those
functions. I imagine most people don't really care how much effort was
required to write fmt.Print. But they would object if asked to write

fmt.PrintB(1, " ")
fmt.PrintS(big.NewInt(2))
fmt.PrintB(" ", 3)

instead of

fmt.Print(1, big.NewInt(2), 3)

> > A better example might be a function that finds the roots of a
> > polynomial with real-valued coefficients
...
> Thanks, it's an interesting example. big.Float isn't really the same
> as float32 or float64. For example, big.Float doesn't provide any way
> to write "a = b + c + d". You can only write, in effect, "a = b + c;
> a += d". And big.Float doesn't have == != < <= >= >. Instead it has
> expressions like "a.Cmp(b) == 0". So if you want to write a generic
> function that works on float32/float64/big.Float, you're going to need
> adaptors no matter what. You can't avoid them.
>
> So in practice I don't think we're going to write the root-finding
> function using parametric polymorphism at all. I think we're going to
> write it using an interface type.

I think we'll need an interface type and parametric polymorphism.

type Float interface{ Add(Float) Float }

doesn't have the right semantics.

> And I think we're going to base
> that interface type on big.Float. Then we need to write an
> implementation of that interface for float32/float64. And that
> implementation is likely to use parametric polymorphism so that we
> only have to write it once to handle both float32 and float64. And
> for that implementation, type lists work fine.

I tried out a few different implementations, evaluating the polynomial
instead of finding roots,
at https://go2goplay.golang.org/p/g8bPHdg5iMd . As far as I can tell,
there is no sensible way to
do it with an interface that is implemented directly by *big.Float; we
need to wrap *big.Float
in a wrapper type in any case. The trouble is that you can write

type Float[type F] interface { Add(F, F) F }

func Sum[type F Float[F]](a, b F) F {

Patrick Smith

unread,
Aug 14, 2020, 2:26:40 AM8/14/20
to Ian Lance Taylor, golang-nuts
Sorry, I accidentally sent the previous message before it was
complete. Please ignore that one.
in a wrapper type in any case. One can write

type Float[type F] interface { Add(F, F) F }

func Sum[type F Float[F]](a, b F) F {
var sum F
return sum.Add(a, b)
}

but this causes a segmentation violation in the call to sum.Add
because sum is nil.

> > What you're looking for here, I think, is a case where there is a type
> > A that implements methods that are isomorphic to the basic arithmetic
> > and/or comparison operators. And then you want an algorithm that can
> > be used with that type, as well as with the builtin types. For such a
> > case, it might be nice to be able to write that algorithm only once.

The big.Float example highlights the danger of looking for examples in
current Go.
Once we have generics, we can write functions that implement
algorithms such as this. Having those algorithms will create an
incentive for new types to be given methods that satisfy the
interfaces used by the algorithms. But without that incentive, people
write their types in whatever way is most convenient for them, with no
eye to standardization.

I think the more important thing is whether the general semantics
match (these are addable things, not the details of how they are
added). Once we have generics, there may be some standardization in
how the methods actually work.

Having said that, note that image.Point has Add and Sub methods
similar to + and -. And https://godoc.org/lukechampine.com/uint128 has
several methods corresponding to builtin operators. I have no idea how
widely used it is.

> > And, of course, with the current design draft, we can write the
> > algorithm only once, provided we write it using methods. We will use
> > the methods defined by the type A. And we will need an adapter for
> > builtin types to implement those methods. And that adaptor can be
> > written using type lists.

From my tests in https://go2goplay.golang.org/p/g8bPHdg5iMd , the way
I would prefer involves a general function to do the operation, and
that accepts one or more adaptors to specify the primitive operations.
The functions for the builtin and user-defined cases would call that
general function.
Looking at the polynomial examples I mentioned above, this seems
mostly right. However, there will often be some other changes needed.
For example, a generic function dealing with a type F that must be
either float32 or float64 can use a plain 5 as a value of type F. If
the function is converted to allow user-defined types with appropriate
operators, then additional code will be needed to convert 5 to type F.
Also, conversions may pose difficulties.

> The particular topic here is whether adopting type lists today is
> going to tie our hands in the future in a way that we don't anticipate
> and that would be bad. It's possible of course, but it's not yet
> clear to me that that must be so.

It's not clear to me either; I just wanted to raise the possibility as
something deserving thought. It now seems to me less likely to be a
serious concern, but I'm still not certain.

Thanks for your thoughts.

Patrick Smith

unread,
Aug 14, 2020, 11:57:38 PM8/14/20
to Ian Lance Taylor, golang-nuts
On Thu, Aug 13, 2020 at 11:25 PM Patrick Smith <pat42...@gmail.com> wrote:
> I tried out a few different implementations, evaluating the polynomial
> instead of finding roots,
> at https://go2goplay.golang.org/p/g8bPHdg5iMd . As far as I can tell,
> there is no sensible way to
> do it with an interface that is implemented directly by *big.Float; we
> need to wrap *big.Float
> in a wrapper type in any case.

After looking at the draft again, and seeing the Settable example
there, I realized we can do it using big.Float directly, but it needs
two interfaces, one matching big.Float and one matching *big.Float. I
have updated my examples at https://go2goplay.golang.org/p/auSkvhWSNHn
.

roger peppe

unread,
Aug 15, 2020, 8:14:30 AM8/15/20
to Patrick Smith, Ian Lance Taylor, golang-nuts
I think that works pretty well, and seems to be the direction that things are going in.
Here's your example cleaned up a bit to show just that latest example, and also
showing how it looks when adapting a built-in type:


It's possible to build a generic adaptor type from the value-style interface to the pointer-style interface, which you might find interesting:


This could almost work on image.Point, except that Point.Mul has the wrong signature.

  cheers,
    rog.

.


--
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.

targe...@gmail.com

unread,
Sep 7, 2020, 2:13:18 AM9/7/20
to golang-nuts
Sorry for interfering. I've gathered statements and expressions which look like candidates for interface-based generalization. Interface and method names are just placeholders. Generic interfaces for collections like slice or map are different beasts. Will post thoughts on them later.

I personally think that most used interface wrappers for predefined types would be range support and generalized collections. Maybe custom key types for maps and custom map hasher.

Comparison operators

 "==", "!=", "<", ">", "<=", ">="

These can be split into two groups - equatable and ordered. Not all types which may be compared equals can be ordered

type Equatable[T] interface {
    Equals(T, T) bool
}
type Ordered[T] interface {
    Compare(T, T) int // < 0 if less, > 0 if greater, 0 if equals
}


Equals can be easily implemented through Ordered via

func[T] (Ordered[T]* self) Equals(left, right T) bool {
    return self.Compare(left right) == 0
}

Possible application for default interface methods but off-topic here

Arithmetic operators

Unary operators "+", "-", "^"

type UnaryOp[T] interface {
    Op(T) T
}

where Op can be Plus, Minus, BitwiseComplement

Binary operators "+", "-", "*", "/", "%", "&", "|", "^", "&^"

type BinaryOp[T] interface {
    Op(T, T) T
}

where Op is "Add", "Sub", "Mul", "Div", "Mod", "BitAnd", "BitOr", "BitXor", "BitAndNot"

Bit-shift operators look almost the same except having different second argument

type BinaryOp[T] interface {
    Op(T, uint) T
}

where "Op" is "ShiftLeft" or "ShiftRight"

Assign-op operators "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "&^="

type BinaryOpAssign[T] interface {
    OpAssign(T)
}

where Op is "Add", "Sub", "Mul", "Div", "Mod", "BitAnd", "BitOr", "BitXor", "BitAndNot"

Default implementation through normal operation would  look like

func[T] (BinaryOp[T]* self) OpAssign(right T) {
    *self = self.Op(*self, right)
}

Except shift operators which would look like

type BinaryOpAssign[T] interface {
    OpAssign(uint)
}

where "Op" is "ShiftLeft" or "ShiftRight"

String concatenation

Uses interfaces "BinaryAdd" and "BinaryAddAssign"

Logical operators

Applied only to booleans, so I see no good reason to add generic interfaces for them

Address operators

Should not have their own interface since they don't assume any kind of type-specific behavior

Receive operator and send statement

May be considered in future. ATM I don't see good application for channel-like types except channels themselves

Increment and decrement

type Incrementable[T] interface {
    Increment()
}
type Decrementable[T] interface {
    Decrement()
}

Assignment

Not subject for generalization since it shouldn't be overridden

For statements with range clause

type Range[K, V] interface {
    Next() K, V, bool
}

Slice's or string's key would be respective index
Channel looks more like exception here since it introduces asynchronous iteration and produces single value.
Reply all
Reply to author
Forward
0 new messages