Can we not invoke methods on types referring to generics?

124 views
Skip to first unread message

Jason E. Aten

unread,
Oct 19, 2023, 8:13:41 PM10/19/23
to golang-nuts
Analogous to

type IntSlice []int

func (p IntSlice) Len() int { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i].val < p[j].val }
func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

and then calling sort.Sort() on an IntSlice, I figured I 
could create a type that is a new name for an instantiated generic type and
then call methods on that new type... but no?

For example:
package main

import "fmt"

type Addable interface {
~complex128 | ~complex64 | ~float64 | ~float32 |
~byte | ~uint16 | ~uint32 | ~uint64 |
~int8 | ~int16 | ~int32 | ~int64 | ~int
}

type Matrix[T Addable] struct {
Nrow int
Ncol int
Dat  []T
}

func (m *Matrix[T]) Col(j int) (res []T) {
for i := 0; i < m.Nrow; i++ {
res = append(res, m.At(i, j))
}
return
}

// At reads out the [i,j]-th element.
func (m *Matrix[T]) At(i, j int) T {
return m.Dat[i*m.Ncol+j]
}

func main() {
m := &Matrix[float64]{
Nrow: 2,
Ncol: 3,
Dat:  make([]float64, 2*3),
}
fmt.Printf("m.At(1,2) = '%v'", m.At(1, 2)) // this is fine, of course.
}

// I assumed this would work... but go 1.21 rejects the call to At() below.
type MatrixF64 Matrix[float64]

func UseMatrixF64(m *MatrixF64) {
       // so why won't this compile? if the same line in main() works?
fmt.Printf("m.At(1,2) = '%v'", m.At(1, 2)) // compile error: m.At undefined (type *MatrixF64 has no field or method At)
}

Ian Lance Taylor

unread,
Oct 19, 2023, 8:51:54 PM10/19/23
to Jason E. Aten, golang-nuts
Using "type MatrixF64 ..." defines a new type that doesn't inherit any
of the method of the old type. That's how type definitions work in
Go. It will work as you expect if you use a type alias: "type
MatrixF64 = ...".

Ian

Nurahmadie Nurahmadie

unread,
Oct 19, 2023, 9:19:06 PM10/19/23
to Ian Lance Taylor, Jason E. Aten, golang-nuts
--
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/CAOyqgcUKo1n3BY5z1OWSFR4omZX4gFXpCgHg5%2BbHrb3jsk7E0w%40mail.gmail.com.

There are two ways to enable this, which are worth discussing further as of why Go decided to behave this way.
  1. To use type alias as mentioned before `type MatrixF64 = Matrix[float64]`, this is generally works but unfortunately, doesn't worth it in the case of aliasing primitives type, i.e. `type String = string` won't allow you to bind new methods to `String` unlike `type String string`. I would like to know why.
  2. To use type embedding such as: type MatrixF64 struct { Matrix[float64] }, since struct/interface embedding will allow the parent struct to call embedded methods/fields directly.

Again, I would love to see the motivation between these design decisions, pretty much I would like to allow new methods to be  attached to type primitives without resorting to manual type casting / type assertion, but that's another topic.

Regards.  

--
regards,
Nurahmadie
--

Ian Lance Taylor

unread,
Oct 19, 2023, 10:59:44 PM10/19/23
to Nurahmadie Nurahmadie, Jason E. Aten, golang-nuts
On Thu, Oct 19, 2023 at 6:18 PM Nurahmadie Nurahmadie
<nurah...@gmail.com> wrote:
>
> There are two ways to enable this, which are worth discussing further as of why Go decided to behave this way.
>
> To use type alias as mentioned before `type MatrixF64 = Matrix[float64]`, this is generally works but unfortunately, doesn't worth it in the case of aliasing primitives type, i.e. `type String = string` won't allow you to bind new methods to `String` unlike `type String string`. I would like to know why.
> To use type embedding such as: type MatrixF64 struct { Matrix[float64] }, since struct/interface embedding will allow the parent struct to call embedded methods/fields directly.
>
>
> Again, I would love to see the motivation between these design decisions, pretty much I would like to allow new methods to be attached to type primitives without resorting to manual type casting / type assertion, but that's another topic.

Adding methods to a primitive type, or more generally adding methods
to a type defined in a different package, would cause different
packages to behave differently depending on whether they see the
methods. That would be confusing. It would meant that type
assertions would sometimes succeed and sometimes fail, depending on
the exact sequence of function calls used.

Ian

Nurahmadie Nurahmadie

unread,
Oct 20, 2023, 12:03:28 AM10/20/23
to Ian Lance Taylor, Jason E. Aten, golang-nuts



Adding methods to a primitive type, or more generally adding methods
to a type defined in a different package, would cause different
packages to behave differently depending on whether they see the
methods.  That would be confusing.  It would meant that type
assertions would sometimes succeed and sometimes fail, depending on
the exact sequence of function calls used.

Ian

Understood, granted due to Go structural typing it's easy to confuse where the methods are coming from.

but given the following, for example,

// this allow new methods to be bounded to `String`
type String string

func Display(s string) {
        fmt.Prinln(s)
}

func main() {
        s := String("hello")
        // this wont work, even though there is enough information that `string` is the underlying type of `String`
        // but if we use `type String = string`, this will work, but no additional method binding is allowed.
        Display(s)

        // for `type String string`, we have to downcast it manually, but we can bind new methods
        Display(string(s)) // this works
}

Is it not possible to have both _auto_ downcasting and new method binding to work in Go?
--
regards,
Nurahmadie
--

Kurtis Rader

unread,
Oct 20, 2023, 12:28:48 AM10/20/23
to Nurahmadie Nurahmadie, Ian Lance Taylor, Jason E. Aten, golang-nuts
You have changed the question. You are no longer asking about defining methods on a type derived from a primitive type. Non-aliased types are deliberately distinct types that cannot be "auto downcast" or "auto upcast". Arguably the biggest flaw of the C language was its automatic type coercion. And I say that as someone who had been programming for several years but didn't find a language I loved until I learned C sometime around 1984. What you are proposing would lead to a huge number of bugs that result from the C/C++ behavior you are advocating for. And that ignores the technical difficulties of doing what you propose unless Go becomes more like C++. Which most Go users, and I'm sure everyone on the Go development team, would argue is a bad idea.

--
Kurtis Rader
Caretaker of the exceptional canines Junior and Hank

Nurahmadie Nurahmadie

unread,
Oct 20, 2023, 12:57:22 AM10/20/23
to Kurtis Rader, Ian Lance Taylor, Jason E. Aten, golang-nuts

You have changed the question. You are no longer asking about defining methods on a type derived from a primitive type. Non-aliased types are deliberately distinct types that cannot be "auto downcast" or "auto upcast". Arguably the biggest flaw of the C language was its automatic type coercion. And I say that as someone who had been programming for several years but didn't find a language I loved until I learned C sometime around 1984. What you are proposing would lead to a huge number of bugs that result from the C/C++ behavior you are advocating for. And that ignores the technical difficulties of doing what you propose unless Go becomes more like C++. Which most Go users, and I'm sure everyone on the Go development team, would argue is a bad idea.

--
Kurtis Rader
Caretaker of the exceptional canines Junior and Hank

Eh, I'm not really proposing anything. I'm interested in the why, I think I worded it badly in my last email, apologize about that, but the change in the question is because the statement that type in different package cannot be bounded/attached with new methods, so, ok, if we need to create new type to attach new methods to these foreign types, why is there no, type coercion, as you said, that allow the new type to be acknowledged as its underlying type? which will not be a question if otherwise Go has mechanism to allow methods to be attached to foreign types. I even deliberately not mentioned another modern language that allows this to a foreign type, not to compare Go with anything.

But ok, I should take the answer as: it will make Go more C/C++ like, and it's bad. Sure.

--
regards,
Nurahmadie
--

Dan Kortschak

unread,
Oct 20, 2023, 1:13:50 AM10/20/23
to golan...@googlegroups.com
On Fri, 2023-10-20 at 11:56 +0700, Nurahmadie Nurahmadie wrote:
> why is there no, type coercion, as you said, that allow the new type
> to be acknowledged as its underlying type? which will not be a
> question if otherwise Go has mechanism to allow methods to be
> attached to foreign types.

There is something like this, if you accept the use of interfaces
instead of concrete types (simplifying here).

You can make a `type T2 struct { T1 }` that has all the methods of T1
and whatever methods you add to T2 (note: Here be dragons!). This is
type embedding.

Kurtis Rader

unread,
Oct 20, 2023, 1:21:27 AM10/20/23
to Nurahmadie Nurahmadie, Ian Lance Taylor, Jason E. Aten, golang-nuts
On Thu, Oct 19, 2023 at 9:56 PM Nurahmadie Nurahmadie <nurah...@gmail.com> wrote:

You have changed the question. You are no longer asking about defining methods on a type derived from a primitive type. Non-aliased types are deliberately distinct types that cannot be "auto downcast" or "auto upcast". Arguably the biggest flaw of the C language was its automatic type coercion. And I say that as someone who had been programming for several years but didn't find a language I loved until I learned C sometime around 1984. What you are proposing would lead to a huge number of bugs that result from the C/C++ behavior you are advocating for. And that ignores the technical difficulties of doing what you propose unless Go becomes more like C++. Which most Go users, and I'm sure everyone on the Go development team, would argue is a bad idea.

Eh, I'm not really proposing anything. I'm interested in the why, I think I worded it badly in my last email, apologize about that, but the change in the question is because the statement that type in different package cannot be bounded/attached with new methods, so, ok, if we need to create new type to attach new methods to these foreign types, why is there no, type coercion, as you said, that allow the new type to be acknowledged as its underlying type? which will not be a question if otherwise Go has mechanism to allow methods to be attached to foreign types. I even deliberately not mentioned another modern language that allows this to a foreign type, not to compare Go with anything.

But ok, I should take the answer as: it will make Go more C/C++ like, and it's bad. Sure.

Personally, I find "why" questions suspicious. They are usually thinly disguised requests to change the existing behavior.  And that appears to be the case for this discussion. Implicit type coercion is almost always a bad idea. That is, more likely to introduce bugs than useful behavior. Here I am not talking about generics which does not introduce type coercion. If you believe the current behavior is wrong you should say so and propose why an alternative behavior is better.

Nurahmadie Nurahmadie

unread,
Oct 20, 2023, 2:01:10 AM10/20/23
to Kurtis Rader, Ian Lance Taylor, Jason E. Aten, golang-nuts



Personally, I find "why" questions suspicious. They are usually thinly disguised requests to change the existing behavior.  And that appears to be the case for this discussion.

No, that's not to be the case, I do imagining the change, but am not thinking of requesting any change, I know the proper workflow of requesting change to Go lang spec, so if it comes down to me offering explanation as of the why the alternative behavior is better then it won't be a discussion here, it will be an RFC.

In this case, I simply don't have the knowledge of Go design decisions and would like to know more about the details behind those decisions.

Anyway, I'll stop here. Thank you for all the replies.

--
regards,
Nurahmadie
--

Bakul Shah

unread,
Oct 20, 2023, 2:07:43 AM10/20/23
to Nurahmadie Nurahmadie, golang-nuts
On Oct 19, 2023, at 9:02 PM, Nurahmadie Nurahmadie <nurah...@gmail.com> wrote:
>
> Is it not possible to have both _auto_ downcasting and new method binding to work in Go?

What you are suggesting may make things more *convenient* but
at the same time the potential for accidental mistakes goes
up. The key is find a happy medium. Not too much discipline,
not too much freedom!


Axel Wagner

unread,
Oct 20, 2023, 5:59:01 AM10/20/23
to golang-nuts
FWIW I think what OP is ultimately asking about is some form of nominal subtyping. When they say "automatic upcasting", they refer (I believe) to what Go calls "assignability", which is in essence a subtype relationship. So they want to be able to define a new type, that is a subtype of an existing type, but add some methods to it.

And - controversially, perhaps - I don't think they would be anything inherently wrong about it. Except that it means we'd have two ways to have subtyping in the language.

First, I agree with other posters here that it would be bad if `type String string` would create a subtype relationship between `String` and `string`. Ultimately, we do want to have the ability to create genuinely new types, with no relationship between them. It's an important safety mechanism. But we could imagine having a new form of type declaration, say `type A < B` (syntax only illustrative) that would create a new type `A`, which inherits all methods from `B`, could add its own and which is assignable to `B` (but not vice-versa). We basically would have three kinds of declarations: 1. `type A B`, introducing no subtype relationship between `A` and `B`, 2. `type A < B`, which makes `A` a subtype of `B` and 3. `type A = B`, which makes them identical (and is conveniently equivalent to `A < B` and `B < A`).

I think this would honestly be fine and perfectly safe. You'd still have to explicitly declare that you want the new type to be a subtype, so you don't get the weak typing of C/C++. And the subtype relationship would only "flow" in one direction, so you can't *arbitrarily* mix them up.

Where difficulties would arise is that it naturally leads people to want to subtype from *multiple* types. E.g. it would make sense wanting to do

type Quadrilateral [4]Point
func (Quadrilateral) Area() float64
type Rhombus < Quadrilateral
func (Rhombus) Angles() (float64, float64)
type Rectangle < Quadrilateral
func (Rectangle) Bounds() (min, max Point)
type Square < (Rhombus, Rectangle) // again, syntax only illustrative)

The issue this creates is that subtype relationships are transitive and in this case would become *path-dependent*. `Square` is a subtype of `Quadrilateral`, but it can get there either via `Rhombus` or via `Rectangle` and it's not clear which way to get there. This matters if `Rhombus` or `Rectangle` (or both) start overwriting methods of `Quadrilateral`. The compiler needs to decide which method to call. Usually it does that by defining some tie-breaks, e.g. "use the type named first in the subtype declaration". But there is a lot of implicity there and with deeper hierarchies, you can get spooky breakages at a distance, if some type in the middle of the hierarchy does some seemingly harmless change like overloading a method. Look up "Python Method Resolution Order" for the kinds of problems that can arise.

Structural subtyping does not have these issues, because the subtype relationship is completely determined by a subset relationship - in Go's case, sets of methods of the dynamic type of the interface. And since it can't override methods, there is no path-dependence - any two methods sets uniquely determine a maximal common subset and a minimum common superset and the path from any interface type to any other interface is unique (structural subtyping is a Lattice).

I think any kind of subtyping relationship *should* ultimately allow you to have multiple super types - these kinds of hierarchies are just far too common to ignore. For example, look at the `io` package - pretty much every combination of `Reader`, `Writer` and `Closer` has some reasonable use cases. I also think there are good technical reasons to avoid the path-dependency pitfall. So it seems to me an easily defensible decision to use structural subtyping as the primary form of subtype relationship, as it allows you to have the benefits without the problems.

We could do both (and disallow multiple inheritance for the nominal subtype relationship). But I also think it's easy to argue that this is redundant and a bit confusing. And ultimately you *can* express most of the useful hierarchies, even if you need a bit more boilerplate.

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

Nurahmadie Nurahmadie

unread,
Oct 20, 2023, 7:07:22 AM10/20/23
to Axel Wagner, golang-nuts
On Fri, 20 Oct 2023 at 16:58, 'Axel Wagner' via golang-nuts <golan...@googlegroups.com> wrote:
FWIW I think what OP is ultimately asking about is some form of nominal subtyping. When they say "automatic upcasting", they refer (I believe) to what Go calls "assignability", which is in essence a subtype relationship. So they want to be able to define a new type, that is a subtype of an existing type, but add some methods to it.

Yes, this is generally what I meant, thank you for rephrasing it more precisely.


And - controversially, perhaps - I don't think they would be anything inherently wrong about it. Except that it means we'd have two ways to have subtyping in the language.

First, I agree with other posters here that it would be bad if `type String string` would create a subtype relationship between `String` and `string`. Ultimately, we do want to have the ability to create genuinely new types, with no relationship between them. It's an important safety mechanism.

This statement doesn't feel right to me, one can always do `type NewType struct{}` to create genuinely new types, but if you do `type String string`, for example, surely you expect String to has `string` value, hence there will always be a relationship between them? I might be missing something obvious here.
 
But we could imagine having a new form of type declaration, say `type A < B` (syntax only illustrative) that would create a new type `A`, which inherits all methods from `B`, could add its own and which is assignable to `B` (but not vice-versa). We basically would have three kinds of declarations: 1. `type A B`, introducing no subtype relationship between `A` and `B`, 2. `type A < B`, which makes `A` a subtype of `B` and 3. `type A = B`, which makes them identical (and is conveniently equivalent to `A < B` and `B < A`).
  
I think it's perfectly makes sense if we resort to nominal subtyping given the new declaration, but I'm genuinely pondering about the existing structural subtyping characteristic instead, and I'm not trying to change anything about Go from the discussion. :D
 
I think this would honestly be fine and perfectly safe. You'd still have to explicitly declare that you want the new type to be a subtype, so you don't get the weak typing of C/C++. And the subtype relationship would only "flow" in one direction, so you can't *arbitrarily* mix them up.

Where difficulties would arise is that it naturally leads people to want to subtype from *multiple* types. E.g. it would make sense wanting to do

type Quadrilateral [4]Point
func (Quadrilateral) Area() float64
type Rhombus < Quadrilateral
func (Rhombus) Angles() (float64, float64)
type Rectangle < Quadrilateral
func (Rectangle) Bounds() (min, max Point)
type Square < (Rhombus, Rectangle) // again, syntax only illustrative)

The issue this creates is that subtype relationships are transitive and in this case would become *path-dependent*. `Square` is a subtype of `Quadrilateral`, but it can get there either via `Rhombus` or via `Rectangle` and it's not clear which way to get there. This matters if `Rhombus` or `Rectangle` (or both) start overwriting methods of `Quadrilateral`. The compiler needs to decide which method to call. Usually it does that by defining some tie-breaks, e.g. "use the type named first in the subtype declaration". But there is a lot of implicity there and with deeper hierarchies, you can get spooky breakages at a distance, if some type in the middle of the hierarchy does some seemingly harmless change like overloading a method. Look up "Python Method Resolution Order" for the kinds of problems that can arise.

Structural subtyping does not have these issues, because the subtype relationship is completely determined by a subset relationship - in Go's case, sets of methods of the dynamic type of the interface. And since it can't override methods, there is no path-dependence - any two methods sets uniquely determine a maximal common subset and a minimum common superset and the path from any interface type to any other interface is unique (structural subtyping is a Lattice).

I think any kind of subtyping relationship *should* ultimately allow you to have multiple super types - these kinds of hierarchies are just far too common to ignore. For example, look at the `io` package - pretty much every combination of `Reader`, `Writer` and `Closer` has some reasonable use cases. I also think there are good technical reasons to avoid the path-dependency pitfall. So it seems to me an easily defensible decision to use structural subtyping as the primary form of subtype relationship, as it allows you to have the benefits without the problems.

Structural subtyping has the most advantage in this use case, multiple super types can be expressed with a new interface (and struct embedding as needed) without changing anything in other packages, so it's not something I'm going to question over again, and I do take some notes about the method resolution issues and the insight about lattice above, thank you for the pointer, I really appreciate it.
 
We could do both (and disallow multiple inheritance for the nominal subtype relationship). But I also think it's easy to argue that this is redundant and a bit confusing. And ultimately you *can* express most of the useful hierarchies, even if you need a bit more boilerplate.

Indeed I do use some boilerplate for these hierarchies, e.g. some abstract types to have `From` and `Into` construct a la Rust.
 

On Fri, Oct 20, 2023 at 8:07 AM Bakul Shah <ba...@iitbombay.org> wrote:
On Oct 19, 2023, at 9:02 PM, Nurahmadie Nurahmadie <nurah...@gmail.com> wrote:
>
> Is it not possible to have both _auto_ downcasting and new method binding to work in Go?

What you are suggesting may make things more *convenient* but
at the same time the potential for accidental mistakes goes
up. The key is find a happy medium. Not too much discipline,
not too much freedom!


--
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/BB7C6A9A-F0BE-4180-B495-93E4B195EA97%40iitbombay.org.

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

Axel Wagner

unread,
Oct 20, 2023, 8:03:23 AM10/20/23
to Nurahmadie Nurahmadie, golang-nuts
On Fri, Oct 20, 2023 at 1:07 PM Nurahmadie Nurahmadie <nurah...@gmail.com> wrote:
This statement doesn't feel right to me, one can always do `type NewType struct{}` to create genuinely new types, but if you do `type String string`, for example, surely you expect String to has `string` value, hence there will always be a relationship between them? I might be missing something obvious here.

I think I distinguish between a type and its representation in my mental model. A type has a semantic meaning going beyond its representation. And when I say "a genuinely new type" I mean - in this context - without implying a subtype-relationship.

For example, we could imagine a world in which we had `type Path string` and `type FilePath string`, for use with `path` and `path/filepath`, respectively. They have the same representation (underlying type, in Go's parlance), but they are semantically different types and they are semantically different from a `string`. We could imagine `os.Open` to take a `FilePath` (instead of a `string`) and for `url.URL.Path` to be a `Path` instead of a `string`. And perhaps the `path` package could use type parameters to manipulate such paths, like `func Join[P Path|FilePath](p ...P) P` and provide helpers to validate and convert between them, like 

func ToSlash(p FilePath) Path
func FromSlash(p Path) FilePath
func Validate(s string) (Path, error)

And we'd hope that this would give us a measure of safety. We would want the compiler to warn us if we pass a `Path` to `os.Open`, prompting us to convert and/or validate it first. To me, we certainly would not want the compiler to just accept us passing one as the other or vice-versa.

Now, it is true that in Go, the representation will imbue some semantic operations on a type. e.g. the language semi-assumes that a `string` is UTF-8 encoded text, as demonstrated by `range` and the conversion to `[]rune`. And part of that is a lack of type-safety, where even with our types, you could assign an arbitrary string literal to a `FilePath`, for example. At the end of the day, Go has always been a more practically minded, than theoretically pure language. But that doesn't disprove the larger point, that there is an advantage to treating types as semantically separate entities, regardless of their representation.

The way the language works right now, `type A B` really means "define a new type A with *the same underlying type* as B". In a way, the actual *type* `B` is inconsequential, only its representation matters. And perhaps that was a mistake, because it conflates a type with its representation. Perhaps we should have required the right-hand side of a type declaration to always be a type-literal - though that would open up the question of how we deal with `string`, `int`,… which currently are defined types on equal footing with what you use a type-declaration for (just that they are predeclared). Perhaps if a type-declaration would have to be clearer about the fact that it associates a completely new type with a *representation*, this confusion would be avoided. But it's not what happened (and TBQH I'm not sure it would've been better).

Brian Candler

unread,
Oct 20, 2023, 8:56:45 AM10/20/23
to golang-nuts
> `type A = B`, which makes them identical (and is conveniently equivalent to `A < B` and `B < A`).

I think that "type A = B" does not introduce a new type at all. It says that A and B are two names for *the same type*.
Reply all
Reply to author
Forward
0 new messages