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: generic programming facilities #15292

Open
adg opened this Issue Apr 14, 2016 · 355 comments

Comments

Projects
None yet
@adg
Contributor

adg commented Apr 14, 2016

This issue proposes that Go should support some form of generic programming.
It has the Go2 label, since for Go1.x the language is more or less done.

Accompanying this issue is a general generics proposal by @ianlancetaylor that includes four specific flawed proposals of generic programming mechanisms for Go.

The intent is not to add generics to Go at this time, but rather to show people what a complete proposal would look like. We hope this will be of help to anyone proposing similar language changes in the future.

@adg adg added Go2 Proposal labels Apr 14, 2016

@gopherbot

This comment has been minimized.

Show comment
Hide comment
@gopherbot

gopherbot commented Apr 14, 2016

CL https://golang.org/cl/22057 mentions this issue.

gopherbot pushed a commit to golang/proposal that referenced this issue Apr 14, 2016

design: add "Go should have generics" proposal
This change proposes the notion of generics in Go, and includes four
proposals from years' past that are each flawed in their own ways.
These historical documents are included to show what a complete
generics proposal looks like.

Updates golang/go#15292

Change-Id: Ice1c35af618a043d55ceec54f182d45a4de780e1
Reviewed-on: https://go-review.googlesource.com/22057
Reviewed-by: Ian Lance Taylor <iant@golang.org>

@ianlancetaylor ianlancetaylor added this to the Proposal milestone Apr 14, 2016

@bradfitz

This comment has been minimized.

Show comment
Hide comment
@bradfitz

bradfitz Apr 14, 2016

Member

Let me preemptively remind everybody of our https://golang.org/wiki/NoMeToo policy. The emoji party is above.

Member

bradfitz commented Apr 14, 2016

Let me preemptively remind everybody of our https://golang.org/wiki/NoMeToo policy. The emoji party is above.

@egonelbre

This comment has been minimized.

Show comment
Hide comment
@egonelbre

egonelbre Apr 14, 2016

Contributor

There is Summary of Go Generics Discussions, which tries to provide an overview of discussions from different places. It also provides some examples how to solve problems, where you would want to use generics.

Contributor

egonelbre commented Apr 14, 2016

There is Summary of Go Generics Discussions, which tries to provide an overview of discussions from different places. It also provides some examples how to solve problems, where you would want to use generics.

@tamird

This comment has been minimized.

Show comment
Hide comment
@tamird

tamird Apr 14, 2016

Contributor

There are two "requirements" in the linked proposal that may complicate the implementation and reduce type safety:

  • Define generic types based on types that are not known until they are instantiated.
  • Do not require an explicit relationship between the definition of a generic type or function and its use. That is, programs should not have to explicitly say type T implements generic G.

These requirements seem to exclude e.g. a system similar to Rust's trait system, where generic types are constrained by trait bounds. Why are these needed?

Contributor

tamird commented Apr 14, 2016

There are two "requirements" in the linked proposal that may complicate the implementation and reduce type safety:

  • Define generic types based on types that are not known until they are instantiated.
  • Do not require an explicit relationship between the definition of a generic type or function and its use. That is, programs should not have to explicitly say type T implements generic G.

These requirements seem to exclude e.g. a system similar to Rust's trait system, where generic types are constrained by trait bounds. Why are these needed?

@sbunce

This comment has been minimized.

Show comment
Hide comment
@sbunce

sbunce Apr 14, 2016

It becomes tempting to build generics into the standard library at a very low level, as in C++ std::basic_string<char, std::char_traits, std::allocator >. This has its benefits—otherwise nobody would do it—but it has wide-ranging and sometimes surprising effects, as in incomprehensible C++ error messages.

The problem in C++ arises from type checking generated code. There needs to be an additional type check before code generation. The C++ concepts proposal enables this by allowing the author of generic code to specify the requirements of a generic type. That way, compilation can fail type checking before code generation and simple error messages can be printed. The problem with C++ generics (without concepts) is that the generic code is the specification of the generic type. That's what creates the incomprehensible error messages.

Generic code should not be the specification of a generic type.

sbunce commented Apr 14, 2016

It becomes tempting to build generics into the standard library at a very low level, as in C++ std::basic_string<char, std::char_traits, std::allocator >. This has its benefits—otherwise nobody would do it—but it has wide-ranging and sometimes surprising effects, as in incomprehensible C++ error messages.

The problem in C++ arises from type checking generated code. There needs to be an additional type check before code generation. The C++ concepts proposal enables this by allowing the author of generic code to specify the requirements of a generic type. That way, compilation can fail type checking before code generation and simple error messages can be printed. The problem with C++ generics (without concepts) is that the generic code is the specification of the generic type. That's what creates the incomprehensible error messages.

Generic code should not be the specification of a generic type.

@ianlancetaylor

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Apr 14, 2016

Contributor

@tamird It is an essential feature of Go's interface types that you can define a non-interface type T and later define an interface type I such that T implements I. See https://golang.org/doc/faq#implements_interface . It would be inconsistent if Go implemented a form of generics for which a generic type G could only be used with a type T that explicitly said "I can be used to implement G."

I'm not familiar with Rust, but I don't know of any language that requires T to explicitly state that it can be used to implement G. The two requirements you mention do not mean that G can not impose requirements on T, just as I imposes requirements on T. The requirements just mean that G and T can be written independently. That is a highly desirable feature for generics, and I can not imagine abandoning it.

Contributor

ianlancetaylor commented Apr 14, 2016

@tamird It is an essential feature of Go's interface types that you can define a non-interface type T and later define an interface type I such that T implements I. See https://golang.org/doc/faq#implements_interface . It would be inconsistent if Go implemented a form of generics for which a generic type G could only be used with a type T that explicitly said "I can be used to implement G."

I'm not familiar with Rust, but I don't know of any language that requires T to explicitly state that it can be used to implement G. The two requirements you mention do not mean that G can not impose requirements on T, just as I imposes requirements on T. The requirements just mean that G and T can be written independently. That is a highly desirable feature for generics, and I can not imagine abandoning it.

@alex

This comment has been minimized.

Show comment
Hide comment
@alex

alex Apr 14, 2016

@ianlancetaylor https://doc.rust-lang.org/book/traits.html explains Rust's traits. While I think they're a good model in general, they would be a bad fit for Go as it exists today.

alex commented Apr 14, 2016

@ianlancetaylor https://doc.rust-lang.org/book/traits.html explains Rust's traits. While I think they're a good model in general, they would be a bad fit for Go as it exists today.

@ianlancetaylor

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Apr 14, 2016

Contributor

@sbunce I also thought that concepts were the answer, and you can see the idea scattered through the various proposals before the last one. But it is discouraging that concepts were originally planned for what became C++11, and it is now 2016, and they are still controversial and not particularly close to being included in the C++ language.

Contributor

ianlancetaylor commented Apr 14, 2016

@sbunce I also thought that concepts were the answer, and you can see the idea scattered through the various proposals before the last one. But it is discouraging that concepts were originally planned for what became C++11, and it is now 2016, and they are still controversial and not particularly close to being included in the C++ language.

@joho

This comment has been minimized.

Show comment
Hide comment
@joho

joho Apr 14, 2016

Would there be value on the academic literature for any guidance on evaluating approaches?

The only paper I've read on the topic is Do developers benefit from generic types? (paywall sorry, you might google your way to a pdf download) which had the following to say

Consequently, a conservative interpretation of the experiment
is that generic types can be considered as a tradeoff
between the positive documentation characteristics and the
negative extensibility characteristics. The exciting part of
the study is that it showed a situation where the use of a
(stronger) static type system had a negative impact on the
development time while at the same time the expected bene-
fit – the reduction of type error fixing time – did not appear.
We think that such tasks could help in future experiments in
identifying the impact of type systems.

I also see #15295 also references Lightweight, flexible object-oriented generics.

If we were going to lean on academia to guide the decision I think it would be better to do an up front literature review, and probably decide early if we would weigh empirical studies differently from ones relying on proofs.

joho commented Apr 14, 2016

Would there be value on the academic literature for any guidance on evaluating approaches?

The only paper I've read on the topic is Do developers benefit from generic types? (paywall sorry, you might google your way to a pdf download) which had the following to say

Consequently, a conservative interpretation of the experiment
is that generic types can be considered as a tradeoff
between the positive documentation characteristics and the
negative extensibility characteristics. The exciting part of
the study is that it showed a situation where the use of a
(stronger) static type system had a negative impact on the
development time while at the same time the expected bene-
fit – the reduction of type error fixing time – did not appear.
We think that such tasks could help in future experiments in
identifying the impact of type systems.

I also see #15295 also references Lightweight, flexible object-oriented generics.

If we were going to lean on academia to guide the decision I think it would be better to do an up front literature review, and probably decide early if we would weigh empirical studies differently from ones relying on proofs.

@benjamingr

This comment has been minimized.

Show comment
Hide comment
@benjamingr

benjamingr Apr 14, 2016

Please see: http://dl.acm.org/citation.cfm?id=2738008 by Barbara Liskov:

The support for generic programming in modern object-oriented programming languages is awkward and lacks desirable expressive power. We introduce an expressive genericity mechanism that adds expressive power and strengthens static checking, while remaining lightweight and simple in common use cases. Like type classes and concepts, the mechanism allows existing types to model type constraints retroactively. For expressive power, we expose models as named constructs that can be defined and selected explicitly to witness constraints; in common uses of genericity, however, types implicitly witness constraints without additional programmer effort.

I think what they did there is pretty cool - I'm sorry if this is the incorrect place to stop but I couldn't find a place to comment in /proposals and I didn't find an appropriate issue here.

benjamingr commented Apr 14, 2016

Please see: http://dl.acm.org/citation.cfm?id=2738008 by Barbara Liskov:

The support for generic programming in modern object-oriented programming languages is awkward and lacks desirable expressive power. We introduce an expressive genericity mechanism that adds expressive power and strengthens static checking, while remaining lightweight and simple in common use cases. Like type classes and concepts, the mechanism allows existing types to model type constraints retroactively. For expressive power, we expose models as named constructs that can be defined and selected explicitly to witness constraints; in common uses of genericity, however, types implicitly witness constraints without additional programmer effort.

I think what they did there is pretty cool - I'm sorry if this is the incorrect place to stop but I couldn't find a place to comment in /proposals and I didn't find an appropriate issue here.

@larsth

This comment has been minimized.

Show comment
Hide comment
@larsth

larsth Apr 15, 2016

It could be interesting to have one or more experimental transpilers - a Go generics source code to Go 1.x.y source code compiler.
I mean - too much talk/arguments-for-my-opinion, and no one is writing source code that try to implement some kind of generics for Go.

Just to get knowledge and experience with Go and generics - to see what works and what doesn't work.
If all Go generics solutions aren't really good, then; No generics for Go.

larsth commented Apr 15, 2016

It could be interesting to have one or more experimental transpilers - a Go generics source code to Go 1.x.y source code compiler.
I mean - too much talk/arguments-for-my-opinion, and no one is writing source code that try to implement some kind of generics for Go.

Just to get knowledge and experience with Go and generics - to see what works and what doesn't work.
If all Go generics solutions aren't really good, then; No generics for Go.

@michael-schaller

This comment has been minimized.

Show comment
Hide comment
@michael-schaller

michael-schaller Apr 15, 2016

Contributor

Can the proposal also include the implications on binary size and memory footprint? I would expect that there will be code duplication for each concrete value type so that compiler optimizations work on them. I hope for a guarantee that there will be no code duplication for concrete pointer types.

Contributor

michael-schaller commented Apr 15, 2016

Can the proposal also include the implications on binary size and memory footprint? I would expect that there will be code duplication for each concrete value type so that compiler optimizations work on them. I hope for a guarantee that there will be no code duplication for concrete pointer types.

@mandolyte

This comment has been minimized.

Show comment
Hide comment
@mandolyte

mandolyte Apr 16, 2016

I offer a Pugh Decision matrix. My criteria include perspicuity impacts (source complexity, size). I also forced ranked the criteria to determine the weights for the criteria. Your own may vary of course. I used "interfaces" as the default alternative and compared this to "copy/paste" generics, template based generics (I had in mind something like how D language works), and something I called runtime instantiation style generics. I'm sure this is a vast over simplification. Nonetheless, it may spark some ideas on how to evaluate choices... this should be a public link to my Google Sheet, here

mandolyte commented Apr 16, 2016

I offer a Pugh Decision matrix. My criteria include perspicuity impacts (source complexity, size). I also forced ranked the criteria to determine the weights for the criteria. Your own may vary of course. I used "interfaces" as the default alternative and compared this to "copy/paste" generics, template based generics (I had in mind something like how D language works), and something I called runtime instantiation style generics. I'm sure this is a vast over simplification. Nonetheless, it may spark some ideas on how to evaluate choices... this should be a public link to my Google Sheet, here

@benjamingr

This comment has been minimized.

Show comment
Hide comment
@benjamingr

benjamingr Apr 16, 2016

Pinging @yizhouzhang and @andrewcmyers so they can voice their opinions about genus like generics in Go. It sounds like it could be a good match :)

benjamingr commented Apr 16, 2016

Pinging @yizhouzhang and @andrewcmyers so they can voice their opinions about genus like generics in Go. It sounds like it could be a good match :)

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Apr 16, 2016

The generics design we came up with for Genus has static, modular type checking, does not require predeclaring that types implement some interface, and comes with reasonable performance. I would definitely look at it if you're thinking about generics for Go. It does seem like a good fit from my understanding of Go.

Here is a link to the paper that doesn't require ACM Digital Library access:
http://www.cs.cornell.edu/andru/papers/genus/

The Genus home page is here: http://www.cs.cornell.edu/projects/genus/

We haven't released the compiler publicly yet, but we are planning to do that fairly soon.

Happy to answer any questions people have.

andrewcmyers commented Apr 16, 2016

The generics design we came up with for Genus has static, modular type checking, does not require predeclaring that types implement some interface, and comes with reasonable performance. I would definitely look at it if you're thinking about generics for Go. It does seem like a good fit from my understanding of Go.

Here is a link to the paper that doesn't require ACM Digital Library access:
http://www.cs.cornell.edu/andru/papers/genus/

The Genus home page is here: http://www.cs.cornell.edu/projects/genus/

We haven't released the compiler publicly yet, but we are planning to do that fairly soon.

Happy to answer any questions people have.

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Apr 16, 2016

In terms of @mandolyte's decision matrix, Genus scores a 17, tied for #1. I would add some more criteria to score, though. For example, modular type checking is important, as others such as @sbunce observed above, but template-based schemes lack it. The technical report for the Genus paper has a much larger table on page 34, comparing various generics designs.

andrewcmyers commented Apr 16, 2016

In terms of @mandolyte's decision matrix, Genus scores a 17, tied for #1. I would add some more criteria to score, though. For example, modular type checking is important, as others such as @sbunce observed above, but template-based schemes lack it. The technical report for the Genus paper has a much larger table on page 34, comparing various generics designs.

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Apr 17, 2016

I just went through the whole Summary of Go Generics document, which was a helpful summary of previous discussions. The generics mechanism in Genus does not, to my mind, suffer from the problems identified for C++, Java, or C#. Genus generics are reified, unlike in Java, so you can find out types at run time. You can also instantiate on primitive types, and you don't get implicit boxing in the places you really don't want it: arrays of T where T is a primitive. The type system is closest to Haskell and Rust -- actually a bit more powerful, but I think also intuitive. Primitive specialization ala C# is not currently supported in Genus but it could be. In most cases, specialization can be determined at link time, so true run-time code generation would not be required.

andrewcmyers commented Apr 17, 2016

I just went through the whole Summary of Go Generics document, which was a helpful summary of previous discussions. The generics mechanism in Genus does not, to my mind, suffer from the problems identified for C++, Java, or C#. Genus generics are reified, unlike in Java, so you can find out types at run time. You can also instantiate on primitive types, and you don't get implicit boxing in the places you really don't want it: arrays of T where T is a primitive. The type system is closest to Haskell and Rust -- actually a bit more powerful, but I think also intuitive. Primitive specialization ala C# is not currently supported in Genus but it could be. In most cases, specialization can be determined at link time, so true run-time code generation would not be required.

@gopherbot

This comment has been minimized.

Show comment
Hide comment
@gopherbot

gopherbot commented Apr 18, 2016

CL https://golang.org/cl/22163 mentions this issue.

mk0x9 pushed a commit to mk0x9/go that referenced this issue Apr 18, 2016

doc: link to iant's generics proposal from the FAQ.
Updates #15292.

Change-Id: I229f66c2a41ae0738225f2ba7a574478f5d6d620
Reviewed-on: https://go-review.googlesource.com/22163
Reviewed-by: Andrew Gerrand <adg@golang.org>

gopherbot pushed a commit that referenced this issue Apr 18, 2016

doc: link to iant's generics proposal from the FAQ.
Updates #15292.

Change-Id: I229f66c2a41ae0738225f2ba7a574478f5d6d620
Reviewed-on: https://go-review.googlesource.com/22163
Reviewed-by: Andrew Gerrand <adg@golang.org>
Reviewed-on: https://go-review.googlesource.com/22166
Reviewed-by: David Symonds <dsymonds@golang.org>
@jba

This comment has been minimized.

Show comment
Hide comment
@jba

jba Apr 18, 2016

A way to constrain generic types that doesn't require adding new language concepts: https://docs.google.com/document/d/1rX4huWffJ0y1ZjqEpPrDy-kk-m9zWfatgCluGRBQveQ/edit?usp=sharing.

jba commented Apr 18, 2016

A way to constrain generic types that doesn't require adding new language concepts: https://docs.google.com/document/d/1rX4huWffJ0y1ZjqEpPrDy-kk-m9zWfatgCluGRBQveQ/edit?usp=sharing.

@jimmyfrasche

This comment has been minimized.

Show comment
Hide comment
@jimmyfrasche

jimmyfrasche Apr 18, 2016

Member

Genus looks really cool and it's clearly an important advancement of the art, but I don't see how it would apply to Go. Does anyone have a sketch of how it would integrate with the Go type system/philosophy?

Member

jimmyfrasche commented Apr 18, 2016

Genus looks really cool and it's clearly an important advancement of the art, but I don't see how it would apply to Go. Does anyone have a sketch of how it would integrate with the Go type system/philosophy?

@sprstnd

This comment has been minimized.

Show comment
Hide comment
@sprstnd

sprstnd Apr 27, 2016

The issue is the go team is stonewalling attempts. The title clearly states the intentions of the go team. And if that wasn't enough to deter all takers, the features demanded of such a broad domain in the proposals by ian make it clear that if you want generics then they don't want you. It is asinine to even attempt dialog with the go team. To those looking for generics in go, I say fracture the language. Begin a new journey- many will follow. I've already seen some great work done in forks. Organize yourselves, rally around a cause

sprstnd commented Apr 27, 2016

The issue is the go team is stonewalling attempts. The title clearly states the intentions of the go team. And if that wasn't enough to deter all takers, the features demanded of such a broad domain in the proposals by ian make it clear that if you want generics then they don't want you. It is asinine to even attempt dialog with the go team. To those looking for generics in go, I say fracture the language. Begin a new journey- many will follow. I've already seen some great work done in forks. Organize yourselves, rally around a cause

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Apr 27, 2016

If anyone wants to try to work up a generics extension to Go based on the Genus design, we are happy to help. We don't know Go well enough to produce a design that harmonizes with the existing language. I think the first step would be a straw-man design proposal with worked-out examples.

andrewcmyers commented Apr 27, 2016

If anyone wants to try to work up a generics extension to Go based on the Genus design, we are happy to help. We don't know Go well enough to produce a design that harmonizes with the existing language. I think the first step would be a straw-man design proposal with worked-out examples.

@mandolyte

This comment has been minimized.

Show comment
Hide comment
@mandolyte

mandolyte Apr 28, 2016

@andrewcmyers hoping that @ianlancetaylor will work with you on that. Just having some examples to look at would help a lot.

mandolyte commented Apr 28, 2016

@andrewcmyers hoping that @ianlancetaylor will work with you on that. Just having some examples to look at would help a lot.

@ianlancetaylor

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Apr 28, 2016

Contributor

I've read through the Genus paper. To the extent that I understand it, it seems nice for Java, but doesn't seem like a natural fit for Go.

One key aspect of Go is that when you write a Go program, most of what you write is code. This is different from C++ and Java, where much more of what you write is types. Genus seems to be mostly about types: you write constraints and models, rather than code. Go's type system is very very simple. Genus's type system is far more complex.

The ideas of retroactive modeling, while clearly useful for Java, do not seem to fit Go at all. People already use adapter types to match existing types to interfaces; nothing further should be needed when using generics.

It would be interesting to see these ideas applied to Go, but I'm not optimistic about the result.

Contributor

ianlancetaylor commented Apr 28, 2016

I've read through the Genus paper. To the extent that I understand it, it seems nice for Java, but doesn't seem like a natural fit for Go.

One key aspect of Go is that when you write a Go program, most of what you write is code. This is different from C++ and Java, where much more of what you write is types. Genus seems to be mostly about types: you write constraints and models, rather than code. Go's type system is very very simple. Genus's type system is far more complex.

The ideas of retroactive modeling, while clearly useful for Java, do not seem to fit Go at all. People already use adapter types to match existing types to interfaces; nothing further should be needed when using generics.

It would be interesting to see these ideas applied to Go, but I'm not optimistic about the result.

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Apr 28, 2016

I'm not a Go expert, but its type system doesn't seem any simpler than pre-generics Java. The type syntax is a bit lighter-weight in a nice way but the underlying complexity seems about the same.

In Genus, constraints are types but models are code. Models are adapters, but they adapt without adding a layer of actual wrapping. This is very useful when you want to, say, adapt an entire array of objects to a new interface. Retroactive modeling lets you treat the array as an array of objects satisfying the desired interface.

andrewcmyers commented Apr 28, 2016

I'm not a Go expert, but its type system doesn't seem any simpler than pre-generics Java. The type syntax is a bit lighter-weight in a nice way but the underlying complexity seems about the same.

In Genus, constraints are types but models are code. Models are adapters, but they adapt without adding a layer of actual wrapping. This is very useful when you want to, say, adapt an entire array of objects to a new interface. Retroactive modeling lets you treat the array as an array of objects satisfying the desired interface.

@jimmyfrasche

This comment has been minimized.

Show comment
Hide comment
@jimmyfrasche

jimmyfrasche Apr 28, 2016

Member

I wouldn't be surprised if it were more complicated than (pre-generics) Java's in a type theoretic sense, even though it's simpler to use in practice.

Relative complexity aside, they're different enough that Genus couldn't map 1:1. No subtyping seems like a big one.

If you're interested:

The briefest summary of the relevant philosophical/design differences I mentioned are contained in the following FAQ entries:

Unlike most languages, the Go spec is very short and clear about the relevant properties of the type system start at https://golang.org/ref/spec#Constants and go straight through until the section titled "Blocks" (all of which is less than 11 pages printed).

Member

jimmyfrasche commented Apr 28, 2016

I wouldn't be surprised if it were more complicated than (pre-generics) Java's in a type theoretic sense, even though it's simpler to use in practice.

Relative complexity aside, they're different enough that Genus couldn't map 1:1. No subtyping seems like a big one.

If you're interested:

The briefest summary of the relevant philosophical/design differences I mentioned are contained in the following FAQ entries:

Unlike most languages, the Go spec is very short and clear about the relevant properties of the type system start at https://golang.org/ref/spec#Constants and go straight through until the section titled "Blocks" (all of which is less than 11 pages printed).

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Apr 28, 2016

Unlike Java and C# generics, the Genus generics mechanism is not based on subtyping. On the other hand, it seems to me that Go does have subtyping, but structural subtyping. That is also a good match for the Genus approach, which has a structural flavor rather than relying on predeclared relationships.

andrewcmyers commented Apr 28, 2016

Unlike Java and C# generics, the Genus generics mechanism is not based on subtyping. On the other hand, it seems to me that Go does have subtyping, but structural subtyping. That is also a good match for the Genus approach, which has a structural flavor rather than relying on predeclared relationships.

@davecheney

This comment has been minimized.

Show comment
Hide comment
@davecheney

davecheney Apr 28, 2016

Contributor

I don't believe that Go has structural subtyping.

While two types whose underlying type is identical are therefore identical
can be substituted for one another, https://play.golang.org/p/cT15aQ-PFr

This does not extend to two types who share a common subset of fields,
https://play.golang.org/p/KrC9_BDXuh.

On Thu, Apr 28, 2016 at 1:09 PM, Andrew Myers notifications@github.com
wrote:

Unlike Java and C# generics, the Genus generics mechanism is not based on
subtyping. On the other hand, it seems to me that Go does have subtyping,
but structural subtyping. That is also a good match for the Genus approach,
which has a structural flavor rather than relying on predeclared
relationships.


You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub
#15292 (comment)

Contributor

davecheney commented Apr 28, 2016

I don't believe that Go has structural subtyping.

While two types whose underlying type is identical are therefore identical
can be substituted for one another, https://play.golang.org/p/cT15aQ-PFr

This does not extend to two types who share a common subset of fields,
https://play.golang.org/p/KrC9_BDXuh.

On Thu, Apr 28, 2016 at 1:09 PM, Andrew Myers notifications@github.com
wrote:

Unlike Java and C# generics, the Genus generics mechanism is not based on
subtyping. On the other hand, it seems to me that Go does have subtyping,
but structural subtyping. That is also a good match for the Genus approach,
which has a structural flavor rather than relying on predeclared
relationships.


You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub
#15292 (comment)

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Apr 28, 2016

Thanks, I was misinterpreting some of the language about when types implement interfaces. Actually, it looks to me as if Go interfaces, with a modest extension, could be used as Genus-style constraints.

andrewcmyers commented Apr 28, 2016

Thanks, I was misinterpreting some of the language about when types implement interfaces. Actually, it looks to me as if Go interfaces, with a modest extension, could be used as Genus-style constraints.

@benjamingr

This comment has been minimized.

Show comment
Hide comment
@benjamingr

benjamingr Apr 28, 2016

That's exactly why I pinged you, genus seems like a much better approach than Java/C# like generics.

benjamingr commented Apr 28, 2016

That's exactly why I pinged you, genus seems like a much better approach than Java/C# like generics.

@egonelbre

This comment has been minimized.

Show comment
Hide comment
@egonelbre

egonelbre Apr 28, 2016

Contributor

There were some ideas with regards to specializing on the interface types; e.g. the package templates approach "proposals" 1 2 are examples of it.

tl;dr; the generic package with interface specialization would look like:

package set
type E interface { Equal(other E) bool }
type Set struct { items []E }
func (s *Set) Add(item E) { ... }

Version 1. with package scoped specialization:

package main
import items set[[E: *Item]]

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs items.Set
xs.Add(&Item{})

Version 2. the declaration scoped specialization:

package main
import set

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs set.Set[[E: *Item]]
xs.Add(&Item{})

The package scoped generics will prevent people from significantly abusing the generics system, since the usage is limited to basic algorithms and data-structures. It basically prevents building new language-abstractions and functional-code.

The declaration scoped specialization has more possibilities at the cost making it more prone to abuse and it is more verbose. But, functional code would be possible, e.g:

type E interface{}
func Reduce(zero E, items []E, fn func(a, b E) E) E { ... }

Reduce[[E: int]](0, []int{1,2,3,4}, func(a, b int)int { return a + b } )
// there are probably ways to have some aliases (or similar) to make it less verbose
alias ReduceInt Reduce[[E: int]]
func ReduceInt Reduce[[E: int]]

The interface specialization approach has interesting properties:

  • Already existing packages using interfaces would be specializable. e.g. I would be able to call sort.Sort[[Interface:MyItems]](...) and have the sorting work on the concrete type instead of interface (with potential gains from inlining).
  • Testing is simplified, I only have to assure that the generic code works with interfaces.
  • It's easy to state how it works. i.e. imagine that [[E: int]] replaces all declarations of E with int.

But, there are verbosity issues when working across packages:

type op
import "set"

type E interface{}
func Union(a, b set.Set[[set.E: E]]) set.Set[[set.E: E]] {
    result := set.New[[set.E: E]]()
    ...
}

Of course, the whole thing is simpler to state than to implement. Internally there are probably tons of problems and ways how it could work.

PS, to the grumblers on slow generics progress, I applaud the Go Team for spending more time on issues that have a bigger benefit to the community e.g. compiler/runtime bugs, SSA, GC, http2.

Contributor

egonelbre commented Apr 28, 2016

There were some ideas with regards to specializing on the interface types; e.g. the package templates approach "proposals" 1 2 are examples of it.

tl;dr; the generic package with interface specialization would look like:

package set
type E interface { Equal(other E) bool }
type Set struct { items []E }
func (s *Set) Add(item E) { ... }

Version 1. with package scoped specialization:

package main
import items set[[E: *Item]]

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs items.Set
xs.Add(&Item{})

Version 2. the declaration scoped specialization:

package main
import set

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs set.Set[[E: *Item]]
xs.Add(&Item{})

The package scoped generics will prevent people from significantly abusing the generics system, since the usage is limited to basic algorithms and data-structures. It basically prevents building new language-abstractions and functional-code.

The declaration scoped specialization has more possibilities at the cost making it more prone to abuse and it is more verbose. But, functional code would be possible, e.g:

type E interface{}
func Reduce(zero E, items []E, fn func(a, b E) E) E { ... }

Reduce[[E: int]](0, []int{1,2,3,4}, func(a, b int)int { return a + b } )
// there are probably ways to have some aliases (or similar) to make it less verbose
alias ReduceInt Reduce[[E: int]]
func ReduceInt Reduce[[E: int]]

The interface specialization approach has interesting properties:

  • Already existing packages using interfaces would be specializable. e.g. I would be able to call sort.Sort[[Interface:MyItems]](...) and have the sorting work on the concrete type instead of interface (with potential gains from inlining).
  • Testing is simplified, I only have to assure that the generic code works with interfaces.
  • It's easy to state how it works. i.e. imagine that [[E: int]] replaces all declarations of E with int.

But, there are verbosity issues when working across packages:

type op
import "set"

type E interface{}
func Union(a, b set.Set[[set.E: E]]) set.Set[[set.E: E]] {
    result := set.New[[set.E: E]]()
    ...
}

Of course, the whole thing is simpler to state than to implement. Internally there are probably tons of problems and ways how it could work.

PS, to the grumblers on slow generics progress, I applaud the Go Team for spending more time on issues that have a bigger benefit to the community e.g. compiler/runtime bugs, SSA, GC, http2.

@jba

This comment has been minimized.

Show comment
Hide comment
@jba

jba Apr 28, 2016

@egonelbre your point that package-level generics will prevent "abuse" is a really important one that I think most people overlook. That plus their relative semantic and syntactic simplicity (only the package and import constructs are affected) make them very attractive for Go.

jba commented Apr 28, 2016

@egonelbre your point that package-level generics will prevent "abuse" is a really important one that I think most people overlook. That plus their relative semantic and syntactic simplicity (only the package and import constructs are affected) make them very attractive for Go.

@jba

This comment has been minimized.

Show comment
Hide comment
@jba

jba Apr 28, 2016

@andrewcymyers interesting that you think Go interfaces work as Genus-style constraints. I would have thought they still have the problem that you can't express multi-type-parameter constraints with them.

One thing I just realized, however, is that in Go you can write an interface inline. So with the right syntax you could put the interface in scope of all the parameters and capture multi-parameter constraints:

type [V, E] Graph [V interface { Edges() E }, E interface { Endpoints() (V, V) }] ...

I think the bigger problem with interfaces as constraints is that methods are not as pervasive in Go as in Java. Built-in types do not have methods. There is no set of universal methods like those in java.lang.Object. Users don't typically define methods like Equals or HashCode on their types unless they specifically need to, because those methods don't qualify a type for use as map keys, or in any algorithm that needs equality.

(Equality in Go is an interesting story. The language gives your type "==" if it meets certain requirements (see https://golang.org/ref/spec#Logical_operators, search for "comparable"). Any type with "==" can serve as a map key. But if your type doesn't deserve "==", then there is nothing you can write that will make it work as a map key.)

Because methods aren't pervasive, and because there is no easy way to express properties of the built-in types (like what operators they work with), I suggested using code itself as the generic constraint mechanism. See the link in my comment of April 18, above. This proposal has its problems, but one nice feature is that generic numeric code could still use the usual operators, instead of cumbersome method calls.

The other way to go is to add methods to types that lack them. You can do this in the existing language in a much lighter way than in Java:

type Int int
func (i Int) Less(j Int) bool { return i < j }

The Int type "inherits" all the operators and other properties of int. Though you have to cast between the two to use Int and int together, which can be a pain.

Genus models could help here. But they would have to be kept very simple. I think @ianlancetaylor was too narrow in his characterization of Go as writing more code, fewer types. The general principal is that Go abhors complexity. We look at Java and C++ and are determined never to go there. (No offense.)

So one quick idea for a model-like feature would be: have the user write types like Int above, and in generic instantiations allow "int with Int", meaning use type int but treat it like Int. Then there is no overt language construct called model, with its keyword, inheritance semantics, and so on. I don't understand models well enough to know whether this is feasible, but it is more in the spirit of Go.

jba commented Apr 28, 2016

@andrewcymyers interesting that you think Go interfaces work as Genus-style constraints. I would have thought they still have the problem that you can't express multi-type-parameter constraints with them.

One thing I just realized, however, is that in Go you can write an interface inline. So with the right syntax you could put the interface in scope of all the parameters and capture multi-parameter constraints:

type [V, E] Graph [V interface { Edges() E }, E interface { Endpoints() (V, V) }] ...

I think the bigger problem with interfaces as constraints is that methods are not as pervasive in Go as in Java. Built-in types do not have methods. There is no set of universal methods like those in java.lang.Object. Users don't typically define methods like Equals or HashCode on their types unless they specifically need to, because those methods don't qualify a type for use as map keys, or in any algorithm that needs equality.

(Equality in Go is an interesting story. The language gives your type "==" if it meets certain requirements (see https://golang.org/ref/spec#Logical_operators, search for "comparable"). Any type with "==" can serve as a map key. But if your type doesn't deserve "==", then there is nothing you can write that will make it work as a map key.)

Because methods aren't pervasive, and because there is no easy way to express properties of the built-in types (like what operators they work with), I suggested using code itself as the generic constraint mechanism. See the link in my comment of April 18, above. This proposal has its problems, but one nice feature is that generic numeric code could still use the usual operators, instead of cumbersome method calls.

The other way to go is to add methods to types that lack them. You can do this in the existing language in a much lighter way than in Java:

type Int int
func (i Int) Less(j Int) bool { return i < j }

The Int type "inherits" all the operators and other properties of int. Though you have to cast between the two to use Int and int together, which can be a pain.

Genus models could help here. But they would have to be kept very simple. I think @ianlancetaylor was too narrow in his characterization of Go as writing more code, fewer types. The general principal is that Go abhors complexity. We look at Java and C++ and are determined never to go there. (No offense.)

So one quick idea for a model-like feature would be: have the user write types like Int above, and in generic instantiations allow "int with Int", meaning use type int but treat it like Int. Then there is no overt language construct called model, with its keyword, inheritance semantics, and so on. I don't understand models well enough to know whether this is feasible, but it is more in the spirit of Go.

@Merovius

This comment has been minimized.

Show comment
Hide comment
@Merovius

Merovius Aug 2, 2018

@aarondl Given that type LinkedList a is already a valid type declaration, I can only see two ways to make this parseable unambiguously: Making the grammar context sensitive (getting into the problems of parsing C, ugh) or using unbounded lookahead (which the go grammar tends to avoid, because of bad error messages in the failure case). I might be misunderstanding something, but IMO that speaks against a token-less approach.

Merovius commented Aug 2, 2018

@aarondl Given that type LinkedList a is already a valid type declaration, I can only see two ways to make this parseable unambiguously: Making the grammar context sensitive (getting into the problems of parsing C, ugh) or using unbounded lookahead (which the go grammar tends to avoid, because of bad error messages in the failure case). I might be misunderstanding something, but IMO that speaks against a token-less approach.

@Merovius

This comment has been minimized.

Show comment
Hide comment
@Merovius

Merovius Aug 2, 2018

@keean Interfaces in Go use methods, not functions. In the specific syntax you suggested, there is nothing that attaches insert to *LinkedList for the compiler (in Haskell that's done via instance declarations). It's also normal for methods to mutate the value they're operating on. None of this is a Show-Stopper, just pointing out that the syntax you're suggesting doesn't work well with Go. Probably more something like

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

Which also demonstrates a couple more questions in regards to how the type parameters are scoped and how this should get parsed.

Merovius commented Aug 2, 2018

@keean Interfaces in Go use methods, not functions. In the specific syntax you suggested, there is nothing that attaches insert to *LinkedList for the compiler (in Haskell that's done via instance declarations). It's also normal for methods to mutate the value they're operating on. None of this is a Show-Stopper, just pointing out that the syntax you're suggesting doesn't work well with Go. Probably more something like

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

Which also demonstrates a couple more questions in regards to how the type parameters are scoped and how this should get parsed.

@Merovius

This comment has been minimized.

Show comment
Hide comment
@Merovius

Merovius Aug 2, 2018

@aarondl there are also more questions I'd have about your proposal. For example, it doesn't allow constraints, so you only get unconstrained polymorphism. Which, in general, isn't really that useful, as you're not allowed to do anything with the values you're getting (e.g. you couldn't implement Collection with a map, as not all types are valid map keys). What should happen when someone tries to do something like that? If it's a compile-time error, does it complain about the instantiation (C++ error messages ahead) or at the definition (you can't do basically anything, because there is nothing that works with all types)?

Merovius commented Aug 2, 2018

@aarondl there are also more questions I'd have about your proposal. For example, it doesn't allow constraints, so you only get unconstrained polymorphism. Which, in general, isn't really that useful, as you're not allowed to do anything with the values you're getting (e.g. you couldn't implement Collection with a map, as not all types are valid map keys). What should happen when someone tries to do something like that? If it's a compile-time error, does it complain about the instantiation (C++ error messages ahead) or at the definition (you can't do basically anything, because there is nothing that works with all types)?

@dc0d

This comment has been minimized.

Show comment
Hide comment
@dc0d

dc0d Aug 2, 2018

@keean Still I fail to understand how a is restricted to be a list (or slice or any other collection). Is this a context-dependent special grammar for collections? If so what is its value? It is not possible to declare user-defined types this way.

dc0d commented Aug 2, 2018

@keean Still I fail to understand how a is restricted to be a list (or slice or any other collection). Is this a context-dependent special grammar for collections? If so what is its value? It is not possible to declare user-defined types this way.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 2, 2018

@Merovius Does that mean Go cannot do multiple-dispatch, and makes the first argument of a 'function' special? This suggests that associated types would be a better fit than multiple-parameter interfaces. Something like this:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt(c Collection, e Collection.Element) {...}

However this still has problems because there is nothing constraining the two collections to be the same type... You would end up needing something like:

func[A] useIt(c A, e A.Element) requires A:Collection

To attempt to explain the difference, multi-parameter interfaces have extra input types that take part in instance selection (hence the connection with multiple-dispatch), whereas associated types are output types, only the receiver type takes part in instance selection, and then the associated types depend on the type of the receiver.

keean commented Aug 2, 2018

@Merovius Does that mean Go cannot do multiple-dispatch, and makes the first argument of a 'function' special? This suggests that associated types would be a better fit than multiple-parameter interfaces. Something like this:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt(c Collection, e Collection.Element) {...}

However this still has problems because there is nothing constraining the two collections to be the same type... You would end up needing something like:

func[A] useIt(c A, e A.Element) requires A:Collection

To attempt to explain the difference, multi-parameter interfaces have extra input types that take part in instance selection (hence the connection with multiple-dispatch), whereas associated types are output types, only the receiver type takes part in instance selection, and then the associated types depend on the type of the receiver.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 2, 2018

@dc0d a and b are type parameters of the interface, just like in a Haskell type class. For something to be considered a Collection it has to define the methods that match the types in the interface where a and b can be any type. However as @Merovius has pointed out, Go interfaces are method based, and do not support multiple-dispatch so multi-parameter interfaces may not be a good fit. With Go's single-dispatch method model, then having associated types in interfaces, instead of multiple-parameters would seem to be a better fit. However the lack of multiple dispatch makes implementing functions like unify(x, y) hard, and you have to use the double-dispatch pattern which is not very nice.

To explain the multi-parameter thing a bit further:

type Cloneable[A] interface {
   clone(x A) A
}

Here a stands for any type, we don't care what it is, as long as the correct functions are defined we consider it Cloneable. We would consider interfaces as constraints on types rather than types themselves.

func clone(x int) int {...}

so in the case of 'clone' we substitute a for int in the interface definition, and we can call clone if the substitution succeeds. This fits nicely with this notation:

func[A] test(x A) A requires Cloneable[A] {...}

This is equivalent to:

type Cloneable interface {
   clone() Cloneable
}

but declares a function not a method, and can be extended with multiple parameters. If you have a language with multiple-dispatch there is nothing special about the first argument of a function/method, so why write it in a different place.

As Go does not have multiple dispatch, this all starts to feel like its too much to change all at once. It seems like associated types would be a better fit, although more limited. This would allow abstract collections, but not elegant solutions to things like unification.

keean commented Aug 2, 2018

@dc0d a and b are type parameters of the interface, just like in a Haskell type class. For something to be considered a Collection it has to define the methods that match the types in the interface where a and b can be any type. However as @Merovius has pointed out, Go interfaces are method based, and do not support multiple-dispatch so multi-parameter interfaces may not be a good fit. With Go's single-dispatch method model, then having associated types in interfaces, instead of multiple-parameters would seem to be a better fit. However the lack of multiple dispatch makes implementing functions like unify(x, y) hard, and you have to use the double-dispatch pattern which is not very nice.

To explain the multi-parameter thing a bit further:

type Cloneable[A] interface {
   clone(x A) A
}

Here a stands for any type, we don't care what it is, as long as the correct functions are defined we consider it Cloneable. We would consider interfaces as constraints on types rather than types themselves.

func clone(x int) int {...}

so in the case of 'clone' we substitute a for int in the interface definition, and we can call clone if the substitution succeeds. This fits nicely with this notation:

func[A] test(x A) A requires Cloneable[A] {...}

This is equivalent to:

type Cloneable interface {
   clone() Cloneable
}

but declares a function not a method, and can be extended with multiple parameters. If you have a language with multiple-dispatch there is nothing special about the first argument of a function/method, so why write it in a different place.

As Go does not have multiple dispatch, this all starts to feel like its too much to change all at once. It seems like associated types would be a better fit, although more limited. This would allow abstract collections, but not elegant solutions to things like unification.

@aarondl

This comment has been minimized.

Show comment
Hide comment
@aarondl

aarondl Aug 2, 2018

@Merovius Thanks for taking a look at the proposal. Let me try to address your concerns. I'm sad you thumbs downed the proposal before we discussed it more, I hope I can change your mind - or maybe you can change mine :)

Unbounded lookahead:
So as I mentioned in the proposal, it currently seems like Go grammar has a good way of detecting the "end" of pretty much everything syntactically. And we still would because of the implicit generic arguments. Single letter lowercase being the syntactical construct that creates that generic argument - or whatever we decide to make that inline token, maybe we even fallback to a tokenized thing like @a in the proposal if we like the syntax enough but it's not possible given compiler difficulty without tokens, though the proposal loses a lot of charm as soon as you do that.

Regardless the problem with type LinkedList a under this proposal isn't that hard because we know that a is a generic type argument and so this would fail with a compiler error the same as type LinkedList fails today with: prog.go:3:16: expected type, found newline (and 1 more errors). The original post didn't really come out and say it but you are not allowed to name a concrete type [a-z]{1} anymore which I -think- solves this problem and is a sacrifice I think we'd all be okay with making (I can only see detriments in creating real types with single letter names in Go code today).

It's just unconstrained polymorphism
The reason I had omitted any kind of traits or generic argument constraints is because I feel that's the role of interfaces in Go, if you would like to do something with a value then that value should be an interface type and not a fully generic type. I think this proposal plays well with interfaces too.

Under this proposal we would still have the same issue as we do now with operators like + so you couldn't make a generic add function for all numeric types, but you could accept a generic add function as an argument. Consider the following:

func Sort(slice []a, compare func (a, a) bool) { ... }

Questions about scoping

You gave an example here:

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

The scope of these identifiers as a rule is bound to the particular declaration/definition they're in. They're shared nowhere and I'm not seeing a reason for them to be.

@keean That's very interesting although as others have pointed out you would have to change what you've shown there to actually be able to implement the interfaces (currently in your example there are no methods with receivers, only functions). Trying to think more about how this affects my original proposal.

aarondl commented Aug 2, 2018

@Merovius Thanks for taking a look at the proposal. Let me try to address your concerns. I'm sad you thumbs downed the proposal before we discussed it more, I hope I can change your mind - or maybe you can change mine :)

Unbounded lookahead:
So as I mentioned in the proposal, it currently seems like Go grammar has a good way of detecting the "end" of pretty much everything syntactically. And we still would because of the implicit generic arguments. Single letter lowercase being the syntactical construct that creates that generic argument - or whatever we decide to make that inline token, maybe we even fallback to a tokenized thing like @a in the proposal if we like the syntax enough but it's not possible given compiler difficulty without tokens, though the proposal loses a lot of charm as soon as you do that.

Regardless the problem with type LinkedList a under this proposal isn't that hard because we know that a is a generic type argument and so this would fail with a compiler error the same as type LinkedList fails today with: prog.go:3:16: expected type, found newline (and 1 more errors). The original post didn't really come out and say it but you are not allowed to name a concrete type [a-z]{1} anymore which I -think- solves this problem and is a sacrifice I think we'd all be okay with making (I can only see detriments in creating real types with single letter names in Go code today).

It's just unconstrained polymorphism
The reason I had omitted any kind of traits or generic argument constraints is because I feel that's the role of interfaces in Go, if you would like to do something with a value then that value should be an interface type and not a fully generic type. I think this proposal plays well with interfaces too.

Under this proposal we would still have the same issue as we do now with operators like + so you couldn't make a generic add function for all numeric types, but you could accept a generic add function as an argument. Consider the following:

func Sort(slice []a, compare func (a, a) bool) { ... }

Questions about scoping

You gave an example here:

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

The scope of these identifiers as a rule is bound to the particular declaration/definition they're in. They're shared nowhere and I'm not seeing a reason for them to be.

@keean That's very interesting although as others have pointed out you would have to change what you've shown there to actually be able to implement the interfaces (currently in your example there are no methods with receivers, only functions). Trying to think more about how this affects my original proposal.

@Merovius

This comment has been minimized.

Show comment
Hide comment
@Merovius

Merovius Aug 3, 2018

Single letter lowercase being the syntactical construct that creates that generic argument

I don't feel good about that; it requires having separate productions for what an identifier is depending on context and also means arbitrarily forbidding certain identifiers for types. But it's not really the time to talk about these details.

Under this proposal we would still have the same issue as we do now with operators like +

I don't understand this sentence. Currently, the + operator doesn't have any of those problems, because the types of its operands are locally known and the error message is clear and unambiguous and points to the source of the problem. Am I correct in assuming that you are saying that you want to disallow any usage of generic values that is not allowed for all possible types (I can't think of a lot of such operations)? And create a compiler error for the offending expression in the generic function? IMO that would limit the value of generics too much.

if you would like to do something with a value then that value should be an interface type and not a fully generic type.

The two main reasons people want generics for, is performance (avoid wrapping of interfaces) and type-safety (making sure that the same type is used in different places, while not caring about which one it is). This seems to ignore those reasons.

you could accept a generic add function as an argument.

True. But pretty unergonomic. Consider how much complaints there where about the sort API. For a lot of generic containers, the amount of functions that the caller would have to implement and pass seems to be prohibitive. Consider, how would a container/heap implementation look under this proposal and how would it be better than the current implementation, in terms of ergonomics? It would seem, the wins are negligible here, at best. You'd have to implement more trivial functions (and duplicate to/reference at each usage site), not fewer.

Merovius commented Aug 3, 2018

Single letter lowercase being the syntactical construct that creates that generic argument

I don't feel good about that; it requires having separate productions for what an identifier is depending on context and also means arbitrarily forbidding certain identifiers for types. But it's not really the time to talk about these details.

Under this proposal we would still have the same issue as we do now with operators like +

I don't understand this sentence. Currently, the + operator doesn't have any of those problems, because the types of its operands are locally known and the error message is clear and unambiguous and points to the source of the problem. Am I correct in assuming that you are saying that you want to disallow any usage of generic values that is not allowed for all possible types (I can't think of a lot of such operations)? And create a compiler error for the offending expression in the generic function? IMO that would limit the value of generics too much.

if you would like to do something with a value then that value should be an interface type and not a fully generic type.

The two main reasons people want generics for, is performance (avoid wrapping of interfaces) and type-safety (making sure that the same type is used in different places, while not caring about which one it is). This seems to ignore those reasons.

you could accept a generic add function as an argument.

True. But pretty unergonomic. Consider how much complaints there where about the sort API. For a lot of generic containers, the amount of functions that the caller would have to implement and pass seems to be prohibitive. Consider, how would a container/heap implementation look under this proposal and how would it be better than the current implementation, in terms of ergonomics? It would seem, the wins are negligible here, at best. You'd have to implement more trivial functions (and duplicate to/reference at each usage site), not fewer.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 3, 2018

@Merovius

thinking about this point from @aarondl

you could accept a generic add function as an argument.

It would be better to have an Addable interface to allow overloading of addition, given some syntax for defining infix operators:

type Addable interface {
   + (x Addable, y Addable) Addable
}

Unfortunately this does not work, because it does not express that we expect all the types to be the same. To define addable we would need something like the multi-parameter interfaces:

type Addable[A] interface {
   + (x A, y A) A
}

Then you would also need Go to do multiple-dispatch which would mean all arguments in a function are treated like a receiver for interface matching. So in the above example any type is Addable if there is a function + defined on it that satisfies the function definitions in the interface definition.

But given those changes you could now write:

type S struct {
   value: int
}

func (+) (x S, y S) S {
   return S {
      value: x.value + y.value
   }
}

func main() {
    println(S {value: 27} + S {value: 5})
}

Of course function overloading and multiple-dispatch may be something that people never want in Go, but then things like defining basic arithmetic on user defined types like vectors, matrices, complex numbers etc, will always be impossible. Like I said above 'associated types' on interfaces would allow some increase in generic programming capability, but not full generality. Is multiple-dispatch (and presumably function overloading) something that could ever happen in Go?

keean commented Aug 3, 2018

@Merovius

thinking about this point from @aarondl

you could accept a generic add function as an argument.

It would be better to have an Addable interface to allow overloading of addition, given some syntax for defining infix operators:

type Addable interface {
   + (x Addable, y Addable) Addable
}

Unfortunately this does not work, because it does not express that we expect all the types to be the same. To define addable we would need something like the multi-parameter interfaces:

type Addable[A] interface {
   + (x A, y A) A
}

Then you would also need Go to do multiple-dispatch which would mean all arguments in a function are treated like a receiver for interface matching. So in the above example any type is Addable if there is a function + defined on it that satisfies the function definitions in the interface definition.

But given those changes you could now write:

type S struct {
   value: int
}

func (+) (x S, y S) S {
   return S {
      value: x.value + y.value
   }
}

func main() {
    println(S {value: 27} + S {value: 5})
}

Of course function overloading and multiple-dispatch may be something that people never want in Go, but then things like defining basic arithmetic on user defined types like vectors, matrices, complex numbers etc, will always be impossible. Like I said above 'associated types' on interfaces would allow some increase in generic programming capability, but not full generality. Is multiple-dispatch (and presumably function overloading) something that could ever happen in Go?

@Merovius

This comment has been minimized.

Show comment
Hide comment
@Merovius

Merovius Aug 3, 2018

things like defining basic arithmetic on user defined types like vectors, matrices, complex numbers etc, will always be impossible.

Some might consider that a feature :) AFAIR there is some proposal or thread floating around somewhere discussing whether it should. FWIW, I think this is - again - wandering off-topic. Operator overloading (or general "how to make Go more Haskell" ideas) isn't really the point of this issue :)

Is multiple-dispatch (and presumably function overloading) something that could ever happen in Go?

Never say never. I wouldn't expect it though, personally.

Merovius commented Aug 3, 2018

things like defining basic arithmetic on user defined types like vectors, matrices, complex numbers etc, will always be impossible.

Some might consider that a feature :) AFAIR there is some proposal or thread floating around somewhere discussing whether it should. FWIW, I think this is - again - wandering off-topic. Operator overloading (or general "how to make Go more Haskell" ideas) isn't really the point of this issue :)

Is multiple-dispatch (and presumably function overloading) something that could ever happen in Go?

Never say never. I wouldn't expect it though, personally.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 3, 2018

@Merovius

Some might consider that a feature :)

Sure, and if Go doesn't do it there are other languages that will :-) Go does not have to be everything to everyone. I was just trying to establish some scope for generics in Go. My focus is creating fully generic languages, as I have an aversion to repeating myself and boilerplate (and I don't like macros). If I had a penny for every time I have had to write a linked list or a tree in 'C' for some specific datatype. It actually makes some projects impossible for a small team because of the volume of code that needs to be held in your head to understand it, and then maintained through changes. Sometimes I think that people that don't get the need for generics just haven't written a large enough program yet. Of course you can instead have a large team of developers working on something and only have each developer responsible for a small part of the total code, but I am interested in making a single developer (or small team) as effective as possible.

Given that function overloading and multiple-dispatch is out of scope, and also given the parsing problems with @aarondl 's suggestion, it seems that adding associated types to interfaces, and type parameters to functions would be about as far as you would want to go with generics in Go.

Something like this would seem to be the right sort of thing:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt<T>(c T, e T.Element) requires T:Collection {...}

Then there would be a decision in the implementation whether to use parametric types or universally quantified types. With parametric types (like Java) then a 'generic' function is not actually a function but some kind of type-safe function template, and as such cannot be passed as an argument unless it is has its type parameter provided so:

f(useIt) // not okay with parametric types
f(useIt<List>) // okay with parametric types

With universally quantified types, you can pass useIt as an argument, and it can then be provided with a type parameter inside f. The reason to favour parametric types is because you can monomorphise the polymorphism at compile time meaning no elaboration of polymorphic functions at runtime. I am not sure this is a concern with Go, because Go is already doing runtime dispatch on interfaces, so as long as the type parameter for useIt implements Collection, you can dispatch to the correct receiver at runtime, so universal quantification is probably the right way for Go.

keean commented Aug 3, 2018

@Merovius

Some might consider that a feature :)

Sure, and if Go doesn't do it there are other languages that will :-) Go does not have to be everything to everyone. I was just trying to establish some scope for generics in Go. My focus is creating fully generic languages, as I have an aversion to repeating myself and boilerplate (and I don't like macros). If I had a penny for every time I have had to write a linked list or a tree in 'C' for some specific datatype. It actually makes some projects impossible for a small team because of the volume of code that needs to be held in your head to understand it, and then maintained through changes. Sometimes I think that people that don't get the need for generics just haven't written a large enough program yet. Of course you can instead have a large team of developers working on something and only have each developer responsible for a small part of the total code, but I am interested in making a single developer (or small team) as effective as possible.

Given that function overloading and multiple-dispatch is out of scope, and also given the parsing problems with @aarondl 's suggestion, it seems that adding associated types to interfaces, and type parameters to functions would be about as far as you would want to go with generics in Go.

Something like this would seem to be the right sort of thing:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt<T>(c T, e T.Element) requires T:Collection {...}

Then there would be a decision in the implementation whether to use parametric types or universally quantified types. With parametric types (like Java) then a 'generic' function is not actually a function but some kind of type-safe function template, and as such cannot be passed as an argument unless it is has its type parameter provided so:

f(useIt) // not okay with parametric types
f(useIt<List>) // okay with parametric types

With universally quantified types, you can pass useIt as an argument, and it can then be provided with a type parameter inside f. The reason to favour parametric types is because you can monomorphise the polymorphism at compile time meaning no elaboration of polymorphic functions at runtime. I am not sure this is a concern with Go, because Go is already doing runtime dispatch on interfaces, so as long as the type parameter for useIt implements Collection, you can dispatch to the correct receiver at runtime, so universal quantification is probably the right way for Go.

@egorse

This comment has been minimized.

Show comment
Hide comment
@egorse

egorse Aug 3, 2018

I wonder, SFINAE mentioned only by @bcmills. Not even mentioned in proposal (though Sort is there as example).
How the Sort for slice and linkedlist might looks like then?

egorse commented Aug 3, 2018

I wonder, SFINAE mentioned only by @bcmills. Not even mentioned in proposal (though Sort is there as example).
How the Sort for slice and linkedlist might looks like then?

@urandom

This comment has been minimized.

Show comment
Hide comment
@urandom

urandom Aug 3, 2018

@keean
I can't figure out how one would define a generic 'Slice' collection with your suggestion. You seem to be defining an 'IntSlice' that might be implementing 'Collection' (though Insert returns a different type than the one wanted by the interface), but that is not a generic 'slice', as it seems to be only for ints, and the method implementations are only for ints. Do we need to define specific implementation per type?

urandom commented Aug 3, 2018

@keean
I can't figure out how one would define a generic 'Slice' collection with your suggestion. You seem to be defining an 'IntSlice' that might be implementing 'Collection' (though Insert returns a different type than the one wanted by the interface), but that is not a generic 'slice', as it seems to be only for ints, and the method implementations are only for ints. Do we need to define specific implementation per type?

@Merovius

This comment has been minimized.

Show comment
Hide comment
@Merovius

Merovius Aug 3, 2018

Sometimes I think that people that don't get the need for generics just haven't written a large enough program yet.

I can assure you that impression is false. And FWIW, ISTM that "the other side" is putting "not seeing the need" into the same bucket as "not seeing the use". I see the use and don't refute it. I don't really see the need, though. I'm doing fine without, even in large codebases.

And don't mistake "wanting them to be done right and pointing out where existing proposals aren't" with "fundamentally opposing the very idea" either.

also given the parsing problems with @aarondl 's suggestion.

As I said, I don't think talking about the parsing problem is really productive right now. Parsing problems can be solved. The lach of constrained polymorphism is far more serious, semantically. IMO, adding generics without that just isn't really worth the effort.

Merovius commented Aug 3, 2018

Sometimes I think that people that don't get the need for generics just haven't written a large enough program yet.

I can assure you that impression is false. And FWIW, ISTM that "the other side" is putting "not seeing the need" into the same bucket as "not seeing the use". I see the use and don't refute it. I don't really see the need, though. I'm doing fine without, even in large codebases.

And don't mistake "wanting them to be done right and pointing out where existing proposals aren't" with "fundamentally opposing the very idea" either.

also given the parsing problems with @aarondl 's suggestion.

As I said, I don't think talking about the parsing problem is really productive right now. Parsing problems can be solved. The lach of constrained polymorphism is far more serious, semantically. IMO, adding generics without that just isn't really worth the effort.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 3, 2018

@urandom

I can't figure out how one would define a generic 'Slice' collection with your suggestion.

As given above you would still need to define a separate implementation for each type of slice, however you would still gain from being able to write algorithms in terms of the generic interface. If you wanted to allow a generic implementation for all slices, you would need to allow parametric associated types and methods. Note I moved the type parameter to after the keyword so it occurs before the receiver type.

type<T> []T.Element = Int

func<T> ([]T) Member(e T) Bool {...}
func<T> ([]T) Insert(e T) Collection {...}

However now you also have to deal with specialisation, because someone could define the associated type and methods for the more specialised []int and you would have to deal with which one to use. Normally you would go with the more specific instance, but it does add another layer of complexity.

I am not sure how much this actually gains you. With my original example above you can write generic algorithms to act on general collections using the interface, and you would only have to provide the methods and associated types for the types you actually use. The major win for me is being able to define algorithms like sort on arbitrary collections and put those algorithms in a library. If I then have a list of "shapes" I just have to define the collection interface methods for my list of shapes, and I can then use any algorithm in the library on them. Being able to define the interface methods for all slice types is of less interest to me, and might be too much complexity for Go?

keean commented Aug 3, 2018

@urandom

I can't figure out how one would define a generic 'Slice' collection with your suggestion.

As given above you would still need to define a separate implementation for each type of slice, however you would still gain from being able to write algorithms in terms of the generic interface. If you wanted to allow a generic implementation for all slices, you would need to allow parametric associated types and methods. Note I moved the type parameter to after the keyword so it occurs before the receiver type.

type<T> []T.Element = Int

func<T> ([]T) Member(e T) Bool {...}
func<T> ([]T) Insert(e T) Collection {...}

However now you also have to deal with specialisation, because someone could define the associated type and methods for the more specialised []int and you would have to deal with which one to use. Normally you would go with the more specific instance, but it does add another layer of complexity.

I am not sure how much this actually gains you. With my original example above you can write generic algorithms to act on general collections using the interface, and you would only have to provide the methods and associated types for the types you actually use. The major win for me is being able to define algorithms like sort on arbitrary collections and put those algorithms in a library. If I then have a list of "shapes" I just have to define the collection interface methods for my list of shapes, and I can then use any algorithm in the library on them. Being able to define the interface methods for all slice types is of less interest to me, and might be too much complexity for Go?

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 3, 2018

@Merovius

I don't really see the need, though. I'm doing fine without, even in large codebases.

If you can cope with a 100,000 line program, then you will be able to do more with 100,000 generic lines than you could with 100,000 non-generic lines (due to the repetition). So you may be a super-star developer able to cope with very large codebases, but you would still achieve more with a very large generic codebase as you would be eliminating the redundancy. That generic program would expand into an even larger non-generic program. It just seems to me that you have not hit your complexity limit yet.

However I think you are right 'need' is too strong, I am happily writing go code, with only occasional frustration about the lack of generics, and I can work around this by simply writing more code, and in Go that code is plesently direct and literal.

The lack of constrained polymorphism is far more serious, semantically. IMO, adding generics without that just isn't really worth the effort.

I agree with this.

keean commented Aug 3, 2018

@Merovius

I don't really see the need, though. I'm doing fine without, even in large codebases.

If you can cope with a 100,000 line program, then you will be able to do more with 100,000 generic lines than you could with 100,000 non-generic lines (due to the repetition). So you may be a super-star developer able to cope with very large codebases, but you would still achieve more with a very large generic codebase as you would be eliminating the redundancy. That generic program would expand into an even larger non-generic program. It just seems to me that you have not hit your complexity limit yet.

However I think you are right 'need' is too strong, I am happily writing go code, with only occasional frustration about the lack of generics, and I can work around this by simply writing more code, and in Go that code is plesently direct and literal.

The lack of constrained polymorphism is far more serious, semantically. IMO, adding generics without that just isn't really worth the effort.

I agree with this.

@Goodwine

This comment has been minimized.

Show comment
Hide comment
@Goodwine

Goodwine Aug 3, 2018

you will be able to do more with 100,000 generic lines than you could with 100,000 non-generic lines (due to the repetition)

I'm curious, from your hypothetical example, what % of those lines would be a generic function?
In my experience this is less than 2% (from a codebase with 115k LOC), so I don't think this is a good argument unless you write a library for "collections"

I do wish we eventually get generics tho

Goodwine commented Aug 3, 2018

you will be able to do more with 100,000 generic lines than you could with 100,000 non-generic lines (due to the repetition)

I'm curious, from your hypothetical example, what % of those lines would be a generic function?
In my experience this is less than 2% (from a codebase with 115k LOC), so I don't think this is a good argument unless you write a library for "collections"

I do wish we eventually get generics tho

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Aug 3, 2018

@keean

Regarding your claim that you cannot do this example in Haskell, here is the code:

This code is not morally equivalent to the code I wrote. It introduces a new Cloneable wrapper type in addition to the ICloneable interface. The Go code did not need a wrapper; nor would other languages that support subtyping.

andrewcmyers commented Aug 3, 2018

@keean

Regarding your claim that you cannot do this example in Haskell, here is the code:

This code is not morally equivalent to the code I wrote. It introduces a new Cloneable wrapper type in addition to the ICloneable interface. The Go code did not need a wrapper; nor would other languages that support subtyping.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 3, 2018

@andrewcmyers

This code is not morally equivalent to the code I wrote. It introduces a new Cloneable wrapper type in addition to the ICloneable interface.

Isn't this what this code does:

type Cloneable interface {...}

It intoduces a data-type 'Cloneable' derived from the interface. You don't see the 'ICloneable' because you don't have instance declarations for interfaces, you just declare the methods.

Can you consider it subtyping when the types that implement an interface do not have to be structurally compatible?

keean commented Aug 3, 2018

@andrewcmyers

This code is not morally equivalent to the code I wrote. It introduces a new Cloneable wrapper type in addition to the ICloneable interface.

Isn't this what this code does:

type Cloneable interface {...}

It intoduces a data-type 'Cloneable' derived from the interface. You don't see the 'ICloneable' because you don't have instance declarations for interfaces, you just declare the methods.

Can you consider it subtyping when the types that implement an interface do not have to be structurally compatible?

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Aug 3, 2018

@keean I would consider Cloneable to be merely a type, not really a "data type". In a language like Java, there would be essentially no added cost to the Cloneable abstraction, because there would be no wrapper, unlike in your code.

It seems to me limiting and undesirable to require structural similarity between types implementing an interface, so I am confused about what you're thinking here.

andrewcmyers commented Aug 3, 2018

@keean I would consider Cloneable to be merely a type, not really a "data type". In a language like Java, there would be essentially no added cost to the Cloneable abstraction, because there would be no wrapper, unlike in your code.

It seems to me limiting and undesirable to require structural similarity between types implementing an interface, so I am confused about what you're thinking here.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 3, 2018

@andrewcmyers
I am using type and data type interchangeably. Any type that can contain data is a data-type.

because there would be no wrapper, unlike in your code.

There is always a wrapper because Go types are always boxed, so the wrapper exists around everything. Haskell needs the wrapper to be explicit because it has unboxed types.

structural similarity between types implementing an interface, so I am confused about what you're thinking here.

Structural subtyping requires the types be 'structurally compatible'. As there is no explicit type hierachy like in an OO language with inheritance, subtyping cannot be nominal, so it must be structural, if it is there at all.

I do see what you mean though, which I would describe as considering an interface to be an abstract base class, not an interface, with some kind of implicit nominal subtype relationship with any type that implements the required methods.

I actually think Go fits both models right now, and it could go either way from here, but I would suggest that calling it an interface not a class suggests a non-subtyping way of thinking.

keean commented Aug 3, 2018

@andrewcmyers
I am using type and data type interchangeably. Any type that can contain data is a data-type.

because there would be no wrapper, unlike in your code.

There is always a wrapper because Go types are always boxed, so the wrapper exists around everything. Haskell needs the wrapper to be explicit because it has unboxed types.

structural similarity between types implementing an interface, so I am confused about what you're thinking here.

Structural subtyping requires the types be 'structurally compatible'. As there is no explicit type hierachy like in an OO language with inheritance, subtyping cannot be nominal, so it must be structural, if it is there at all.

I do see what you mean though, which I would describe as considering an interface to be an abstract base class, not an interface, with some kind of implicit nominal subtype relationship with any type that implements the required methods.

I actually think Go fits both models right now, and it could go either way from here, but I would suggest that calling it an interface not a class suggests a non-subtyping way of thinking.

@Merovius

This comment has been minimized.

Show comment
Hide comment
@Merovius

Merovius Aug 3, 2018

@keean I don't understand your comment. First you tell me you disagree and that I "just haven't met my complexity limit yet" and then you tell me you agree (in that "need" is too strong a word). I also think your argument is fallacious (you assume LOC is the primary measure of complexity and that every line of code is equal). But most of all, I don't think the "who is writing more complicated programs" is really a productive line of discussion. I was just trying to clarify, that the argument "if you disagree with me, that must mean you are not working on as hard or interesting problems" isn't convincing and does not come off as in good faith. I hope you can just trust that people can disagree with you about the importance of this feature while being equally competent and doing just as interesting things.

Merovius commented Aug 3, 2018

@keean I don't understand your comment. First you tell me you disagree and that I "just haven't met my complexity limit yet" and then you tell me you agree (in that "need" is too strong a word). I also think your argument is fallacious (you assume LOC is the primary measure of complexity and that every line of code is equal). But most of all, I don't think the "who is writing more complicated programs" is really a productive line of discussion. I was just trying to clarify, that the argument "if you disagree with me, that must mean you are not working on as hard or interesting problems" isn't convincing and does not come off as in good faith. I hope you can just trust that people can disagree with you about the importance of this feature while being equally competent and doing just as interesting things.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 3, 2018

@Merovius
I was saying you are likely a more capable programmer than I am, and thus able to work with more complexity. I certainly don't think you are working on less interesting or less complex problems, and I am sorry it came across that way. I spent yesterday trying to get a scanner working, which was a very uninteresting problem.

I can think that generics help me write more complex programs with my limited brainpower, and also admit that I don't "need" generics. It's a question of degree. I can still program without generics, but I can't necessarily write software of the same complexity.

I hope that reassures you I am acting in good faith, I have no hidden agenda here, and if Go does not adopt generics I will still use it. I have an opinion about the best way to do generics, but it's not the only opinion, I can only talk from my own experience. If I'm not helping there are plenty of other things I can spend my time on, so just say the word, and I will refocus elsewhere.

keean commented Aug 3, 2018

@Merovius
I was saying you are likely a more capable programmer than I am, and thus able to work with more complexity. I certainly don't think you are working on less interesting or less complex problems, and I am sorry it came across that way. I spent yesterday trying to get a scanner working, which was a very uninteresting problem.

I can think that generics help me write more complex programs with my limited brainpower, and also admit that I don't "need" generics. It's a question of degree. I can still program without generics, but I can't necessarily write software of the same complexity.

I hope that reassures you I am acting in good faith, I have no hidden agenda here, and if Go does not adopt generics I will still use it. I have an opinion about the best way to do generics, but it's not the only opinion, I can only talk from my own experience. If I'm not helping there are plenty of other things I can spend my time on, so just say the word, and I will refocus elsewhere.

@aarondl

This comment has been minimized.

Show comment
Hide comment
@aarondl

aarondl Aug 5, 2018

@Merovius Thanks for the continued dialog.

| The two main reasons people want generics for, is performance (avoid wrapping of interfaces) and type-safety (making sure that the same type is used in different places, while not caring about which one it is). This seems to ignore those reasons.

Maybe we're looking at what I've proposed very differently, as from my perspective it does both of these things as far as I can tell? In the linked list example there is no wrapping with interfaces and therefore it should as performant as if hand-written for a given type. In the type-safety side it is the same. Is there a counter-example you can give here to help me understand where you're coming from?

| True. But pretty unergonomic. Consider how much complaints there where about the sort API. For a lot of generic containers, the amount of functions that the caller would have to implement and pass seems to be prohibitive. Consider, how would a container/heap implementation look under this proposal and how would it be better than the current implementation, in terms of ergonomics? It would seem, the wins are negligible here, at best. You'd have to implement more trivial functions (and duplicate to/reference at each usage site), not fewer.

I'm actually not concerned by this at all. I don't believe that the amount of functions would be prohibitive but I'm definitely opened to seeing some counter-examples. Recall that the API that people complained about was not one that you had to provide a function for but the original one here: https://golang.org/pkg/sort/#Interface where you needed to create a new type that was simply your slice + type, and then implement 3 methods on it. In light of the complaints and the pain associated with this interface the following was created: https://golang.org/pkg/sort/#Slice, I for one have no problem with this API and we would recover the performance penalties of this under the proposal we're discussing by simply altering the definition to func Slice(slice []a, less func(a, a) bool).

In terms of the container/heap data structure no matter what Generic proposal you accept that needs an entire rewrite. container/heap just like the sort package is just providing algorithms on top of your own data structure, but neither package ever owns the data structure because otherwise we'd have []interface{} and the costs associated with that. Presumably we would change them since you would be able to have a Heap that owns a slice with a concrete type thanks to generics, and this is true under any of the proposals I've seen here (including my own).

I'm trying to tease apart the differences in our perspectives on what I've proposed are. And I think the root of the disagreement (past any personal preference syntactically) is that there's no constraints on the Generic types. But I'm still trying to figure out what that gains us. If the answer is that nothing where performance is concerned is allowed to use an interface then there's not a lot I can say here.

Consider the following hash table definition:

// Hasher turns a key into a hash
type Hasher interface {
  func Hash() []byte
}

type HashTable v struct {
   Keys   []Hasher
   Values []v
}

// Note that the generic arguments must be repeated here and immediately
// understood without reading another line of code, which to me
// is a readability win over the sudden appearance of the K and V which are
// defined elsewhere in the code in the example below. This is of course because
// the tokenized type declarations with constraints are fairly painful in general
// and repeating them everywhere is simply too much.
func (h (*HashTable v)) Insert(key Hasher, value v) { ... }

Are we saying that the []Hasher is a non-starter due to performance/storage concerns and that in order to have a successful Generics implementation in Go we absolutely must have something like the following?

// Without selecting another proposal I have no idea how the constraint might be defined or implemented so let's just pretend
type [K: Hasher, V] HashTable a struct {
   Keys   []K
   Values []V
}

func (h *HashTable) Insert(key K, value V) { ... }

Hopefully you see where I'm coming from. But it's definitely possible that I don't understand the constraints that you wish to impose upon certain code. Maybe there's use cases I haven't considered, regardless I hope to come to a fuller understanding of what the requirements are and how the proposal is failing them.

aarondl commented Aug 5, 2018

@Merovius Thanks for the continued dialog.

| The two main reasons people want generics for, is performance (avoid wrapping of interfaces) and type-safety (making sure that the same type is used in different places, while not caring about which one it is). This seems to ignore those reasons.

Maybe we're looking at what I've proposed very differently, as from my perspective it does both of these things as far as I can tell? In the linked list example there is no wrapping with interfaces and therefore it should as performant as if hand-written for a given type. In the type-safety side it is the same. Is there a counter-example you can give here to help me understand where you're coming from?

| True. But pretty unergonomic. Consider how much complaints there where about the sort API. For a lot of generic containers, the amount of functions that the caller would have to implement and pass seems to be prohibitive. Consider, how would a container/heap implementation look under this proposal and how would it be better than the current implementation, in terms of ergonomics? It would seem, the wins are negligible here, at best. You'd have to implement more trivial functions (and duplicate to/reference at each usage site), not fewer.

I'm actually not concerned by this at all. I don't believe that the amount of functions would be prohibitive but I'm definitely opened to seeing some counter-examples. Recall that the API that people complained about was not one that you had to provide a function for but the original one here: https://golang.org/pkg/sort/#Interface where you needed to create a new type that was simply your slice + type, and then implement 3 methods on it. In light of the complaints and the pain associated with this interface the following was created: https://golang.org/pkg/sort/#Slice, I for one have no problem with this API and we would recover the performance penalties of this under the proposal we're discussing by simply altering the definition to func Slice(slice []a, less func(a, a) bool).

In terms of the container/heap data structure no matter what Generic proposal you accept that needs an entire rewrite. container/heap just like the sort package is just providing algorithms on top of your own data structure, but neither package ever owns the data structure because otherwise we'd have []interface{} and the costs associated with that. Presumably we would change them since you would be able to have a Heap that owns a slice with a concrete type thanks to generics, and this is true under any of the proposals I've seen here (including my own).

I'm trying to tease apart the differences in our perspectives on what I've proposed are. And I think the root of the disagreement (past any personal preference syntactically) is that there's no constraints on the Generic types. But I'm still trying to figure out what that gains us. If the answer is that nothing where performance is concerned is allowed to use an interface then there's not a lot I can say here.

Consider the following hash table definition:

// Hasher turns a key into a hash
type Hasher interface {
  func Hash() []byte
}

type HashTable v struct {
   Keys   []Hasher
   Values []v
}

// Note that the generic arguments must be repeated here and immediately
// understood without reading another line of code, which to me
// is a readability win over the sudden appearance of the K and V which are
// defined elsewhere in the code in the example below. This is of course because
// the tokenized type declarations with constraints are fairly painful in general
// and repeating them everywhere is simply too much.
func (h (*HashTable v)) Insert(key Hasher, value v) { ... }

Are we saying that the []Hasher is a non-starter due to performance/storage concerns and that in order to have a successful Generics implementation in Go we absolutely must have something like the following?

// Without selecting another proposal I have no idea how the constraint might be defined or implemented so let's just pretend
type [K: Hasher, V] HashTable a struct {
   Keys   []K
   Values []V
}

func (h *HashTable) Insert(key K, value V) { ... }

Hopefully you see where I'm coming from. But it's definitely possible that I don't understand the constraints that you wish to impose upon certain code. Maybe there's use cases I haven't considered, regardless I hope to come to a fuller understanding of what the requirements are and how the proposal is failing them.

@Merovius

This comment has been minimized.

Show comment
Hide comment
@Merovius

Merovius Aug 5, 2018

Maybe we're looking at what I've proposed very differently, as from my perspective it does both of these things as far as I can tell?

The "this" in the section you are quoting is referring to using interfaces. The issue isn't, that your proposal doesn't do either, it's that your proposal doesn't allow constrained polymorphism, which excludes most usages for them. And the alternative you suggested for that where interfaces, which don't really address the core use-case for generics either (because of the two things I mentioned).

For example, your proposal (as originally written) did not actually allow writing a generic map of any sorts, as that would require to be able to at least compare keys using == (which is a constraint, so implementing a map requires constrained polymorphism).

In light of the complaints and the pain associated with this interface the following was created: https://golang.org/pkg/sort/#Slice

Note, that this interface still isn't possible in your proposal of generics, as it relies on reflection for length and swapping (so, again, you have a constraint on slice-operations). Even if we accept that API as the lower bound of what generics should be able to accomplish (lots of people wouldn't. There are still plenty of complaints about the lack of type-safety in that API), your proposal wouldn't pass that bar.

But also, again, you are quoting a response to a specific point you made, namely that you could get constrained polymorphism by passing function literals in the API. And that specific way you suggested to work around the lack of constrained polymorphism would require implementing more-or-less the old API. i.e. you are quoting my response to this argument, which you are then just repeating:

we would recover the performance penalties of this under the proposal we're discussing by simply altering the definition to func Slice(slice []a, less func(a, a) bool).

That's the old API though. You are saying "my proposal doesn't allow constrained polymorphism, but that's no problem, because we can just not use generics and instead use the existing solutions (reflection/interfaces) instead". Well, responding to "your proposal doesn't allow the most basic use cases that people want generics for" with "we can just do the things people are already doing without generics for those most basic use cases" doesn't seem to get us anywhere, TBH. A generics proposal that doesn't help you to write even basic container types, sort, max… just doesn't seem worth it.

this is true under any of the proposals I've seen here (including my own).

Most generics proposals include some way to constrain type-parameters. i.e. to express "the type parameter has to have a Less method", or "the type parameter must be comparable". Yours - AFAICT - doesn't.

Consider the following hash table definition:

Your definition is incomplete. a) The key type also needs equality and b) you are not preventing using different key types. i.e. this would be legal:

type hasherA uint64

func (a hasherA) Hash() []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(a))
    return b
}

type hasherB string

func (b hasherB) Hash() []byte {
    return []byte(b)
}

h := new(HashTable int)
h.Insert(hasherA(42), 1)
h.Insert(hasherB("Hello world"), 2)

It shouldn't be legal though, as you are using different key types. i.e. the container is not type-checked to the degree that people want. You need to parameterize hashtable over both key and value type

type HashTable k v struct {
    Keys []k
    Values []v
}

func (h *(HashTable k v)) Insert(key k, value v) {
    // You can't actually do anything with k, as it's unconstrained. i.e. you can't hash it, compare it…
    // Implementing this is impossible in your proposal.
}

// If it weren't impossible, you'd get this:
h := new(HashTable hasherA int)
h[hasherA(42)] = 1
h[hasherB("Hello world")] = 2 // compile error - can't use hasherB as hasherA

Or, if it helps, imagine you are trying to implement a hash-set. You'd get the same issue but now the resulting container doesn't have any additional type-checking over interface{}.

This is why your proposal doesn't address the most basic use-cases: It relies on interfaces to constrain polymorphism, but then doesn't actually provide any way to check those interfaces for consistency. You can either have consistent type-checking or have constrained polymorphism, but not both. But you need both.

that in order to have a successful Generics implementation in Go we absolutely must have something like the following?

It's at least how I feel about that, yeah, pretty much. If a proposal doesn't allow writing type-safe containers or sort or… it doesn't really add anything to the existing language that is significant enough to justify the cost.

Merovius commented Aug 5, 2018

Maybe we're looking at what I've proposed very differently, as from my perspective it does both of these things as far as I can tell?

The "this" in the section you are quoting is referring to using interfaces. The issue isn't, that your proposal doesn't do either, it's that your proposal doesn't allow constrained polymorphism, which excludes most usages for them. And the alternative you suggested for that where interfaces, which don't really address the core use-case for generics either (because of the two things I mentioned).

For example, your proposal (as originally written) did not actually allow writing a generic map of any sorts, as that would require to be able to at least compare keys using == (which is a constraint, so implementing a map requires constrained polymorphism).

In light of the complaints and the pain associated with this interface the following was created: https://golang.org/pkg/sort/#Slice

Note, that this interface still isn't possible in your proposal of generics, as it relies on reflection for length and swapping (so, again, you have a constraint on slice-operations). Even if we accept that API as the lower bound of what generics should be able to accomplish (lots of people wouldn't. There are still plenty of complaints about the lack of type-safety in that API), your proposal wouldn't pass that bar.

But also, again, you are quoting a response to a specific point you made, namely that you could get constrained polymorphism by passing function literals in the API. And that specific way you suggested to work around the lack of constrained polymorphism would require implementing more-or-less the old API. i.e. you are quoting my response to this argument, which you are then just repeating:

we would recover the performance penalties of this under the proposal we're discussing by simply altering the definition to func Slice(slice []a, less func(a, a) bool).

That's the old API though. You are saying "my proposal doesn't allow constrained polymorphism, but that's no problem, because we can just not use generics and instead use the existing solutions (reflection/interfaces) instead". Well, responding to "your proposal doesn't allow the most basic use cases that people want generics for" with "we can just do the things people are already doing without generics for those most basic use cases" doesn't seem to get us anywhere, TBH. A generics proposal that doesn't help you to write even basic container types, sort, max… just doesn't seem worth it.

this is true under any of the proposals I've seen here (including my own).

Most generics proposals include some way to constrain type-parameters. i.e. to express "the type parameter has to have a Less method", or "the type parameter must be comparable". Yours - AFAICT - doesn't.

Consider the following hash table definition:

Your definition is incomplete. a) The key type also needs equality and b) you are not preventing using different key types. i.e. this would be legal:

type hasherA uint64

func (a hasherA) Hash() []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(a))
    return b
}

type hasherB string

func (b hasherB) Hash() []byte {
    return []byte(b)
}

h := new(HashTable int)
h.Insert(hasherA(42), 1)
h.Insert(hasherB("Hello world"), 2)

It shouldn't be legal though, as you are using different key types. i.e. the container is not type-checked to the degree that people want. You need to parameterize hashtable over both key and value type

type HashTable k v struct {
    Keys []k
    Values []v
}

func (h *(HashTable k v)) Insert(key k, value v) {
    // You can't actually do anything with k, as it's unconstrained. i.e. you can't hash it, compare it…
    // Implementing this is impossible in your proposal.
}

// If it weren't impossible, you'd get this:
h := new(HashTable hasherA int)
h[hasherA(42)] = 1
h[hasherB("Hello world")] = 2 // compile error - can't use hasherB as hasherA

Or, if it helps, imagine you are trying to implement a hash-set. You'd get the same issue but now the resulting container doesn't have any additional type-checking over interface{}.

This is why your proposal doesn't address the most basic use-cases: It relies on interfaces to constrain polymorphism, but then doesn't actually provide any way to check those interfaces for consistency. You can either have consistent type-checking or have constrained polymorphism, but not both. But you need both.

that in order to have a successful Generics implementation in Go we absolutely must have something like the following?

It's at least how I feel about that, yeah, pretty much. If a proposal doesn't allow writing type-safe containers or sort or… it doesn't really add anything to the existing language that is significant enough to justify the cost.

@aarondl

This comment has been minimized.

Show comment
Hide comment
@aarondl

aarondl Aug 5, 2018

@Merovius Okay. I think I've got an understanding of what you want. Keep in mind that your use cases are very far away from what I want. I'm not really itching for type safe containers though I suspect - as you stated - that may be a minority opinion. A few of the biggest things that I'd like to see are result types instead of errors and easy slice manipulation without duplication or reflection everywhere which my proposal does a reasonable job of addressing. However, I can see how from your perspective it "doesn't address the most basic use-cases" if your basic use-case is writing generic containers without the use of interfaces,

Note, that this interface still isn't possible in your proposal of generics, as it relies on reflection for length and swapping (so, again, you have a constraint on slice-operations). Even if we accept that API as the lower bound of what generics should be able to accomplish (lots of people wouldn't. There are still plenty of complaints about the lack of type-safety in that API), your proposal wouldn't pass that bar.

Reading this it's clear you've thoroughly misunderstood the way the generic slices would/should work under this proposal. It's through this misunderstanding that you've come to the false conclusion that "this interface still isn't possible in your proposal". Under any proposal a generic slice must be possible, this is what I think. And len() in the world as I saw it would be defined as: func len(slice []a), which is a generic slice argument meaning that it can count length in a non-reflection way for any slice. This is a lot of the point of this proposal as I said above (easy slice manipulation) and I'm sorry I wasn't able to convey that well through the examples I gave and the gist I made. A generic slice should be able to be used as easily as an []int is today, I'll say again that any proposal that doesn't address this (slice/array swaps, assignment, len, cap, etc.) is falling short in my opinion.

All that said, now we're really clear about what each other's goals are. When I proposed what I did I very much said that it was simply a syntactical proposal and that the details were super fuzzy. But we sort of got into the details anyway and one of those details ended up being the lack of constraints, when I wrote it up I just didn't have them in mind because they're not important for what I'd like to do, it's not to say we couldn't add them or that they're not desirable. The main problem with continuing with the proposed syntax and trying to shoehorn constraints in would be that the definition of a generic argument currently repeats itself (intentionally) so there is no referring to code elsewhere to determine constraints etc. If we were to introduce constraints I don't see how we could keep this.

The best counter-example is that sort function we were discussing earlier.

type Sort(slice []a:Lesser, less func(a:Lesser, a:Lesser)) { ... }

As you can see there's no nice way to make this happen, and the token-spam approaches to Generics start to sound better again. In order to define constraints on these we need to change two things from the original proposal:

  • There needs to be a way to point at a type argument and give it constraints.
  • The constraints need to last for longer than a single definition, perhaps that scope is a type, perhaps that scope is a file (file actually sounds pretty reasonable).

Disclaimer: The following isn't an actual amendment to the proposal because I'm just throwing random symbols out there, I'm just using these syntaxes as examples to illustrate what we could do to amend the proposal as it stands originally

// Decorator style, follows the definition of the type thorugh all
// of it's methods.
@a: Lesser, Hasher, Equaler
func Sort(slice []a) { ... }
@k: Equaler, Hasher
type HashTable k v struct

// Inline, follows the definition of the type through
// all of it's methods.
func [a: Hasher, Equaler] Sort(slice []a) { ... }
type [k: Hasher, Equaler] HashTable k v struct

// File-scope global style, if k appears as a generic argument
// it's constrained by this that appears at the top of the file underneath
// the imports but before any other code.
@k: Equaler, Hasher

Again note that none of the above I actually want to add to the proposal really. I'm just showing what sort of constructs we could use to solve the problem, and how they look is somewhat irrelevant right now.

The question we then need to answer is: Do we still gain value from the implicit generic arguments? The main point of the proposal was to keep the clean Go-like feel of the language, to keep things simple, to keep things sufficiently low noise by eliminating excessive tokens. In the many cases where there are no constraints necessary, for example a map function or the definition of a Result type, does it look good, does it feel like Go, is it useful? Assuming that constraints are also available in some form or another.

func map(slice []a, mapper func(a) b) {
  for i := range slice {
    slice[i] = mapper(slice[i])
  }
}

type Result a b struct {
  Ok  a
  Err b
}

aarondl commented Aug 5, 2018

@Merovius Okay. I think I've got an understanding of what you want. Keep in mind that your use cases are very far away from what I want. I'm not really itching for type safe containers though I suspect - as you stated - that may be a minority opinion. A few of the biggest things that I'd like to see are result types instead of errors and easy slice manipulation without duplication or reflection everywhere which my proposal does a reasonable job of addressing. However, I can see how from your perspective it "doesn't address the most basic use-cases" if your basic use-case is writing generic containers without the use of interfaces,

Note, that this interface still isn't possible in your proposal of generics, as it relies on reflection for length and swapping (so, again, you have a constraint on slice-operations). Even if we accept that API as the lower bound of what generics should be able to accomplish (lots of people wouldn't. There are still plenty of complaints about the lack of type-safety in that API), your proposal wouldn't pass that bar.

Reading this it's clear you've thoroughly misunderstood the way the generic slices would/should work under this proposal. It's through this misunderstanding that you've come to the false conclusion that "this interface still isn't possible in your proposal". Under any proposal a generic slice must be possible, this is what I think. And len() in the world as I saw it would be defined as: func len(slice []a), which is a generic slice argument meaning that it can count length in a non-reflection way for any slice. This is a lot of the point of this proposal as I said above (easy slice manipulation) and I'm sorry I wasn't able to convey that well through the examples I gave and the gist I made. A generic slice should be able to be used as easily as an []int is today, I'll say again that any proposal that doesn't address this (slice/array swaps, assignment, len, cap, etc.) is falling short in my opinion.

All that said, now we're really clear about what each other's goals are. When I proposed what I did I very much said that it was simply a syntactical proposal and that the details were super fuzzy. But we sort of got into the details anyway and one of those details ended up being the lack of constraints, when I wrote it up I just didn't have them in mind because they're not important for what I'd like to do, it's not to say we couldn't add them or that they're not desirable. The main problem with continuing with the proposed syntax and trying to shoehorn constraints in would be that the definition of a generic argument currently repeats itself (intentionally) so there is no referring to code elsewhere to determine constraints etc. If we were to introduce constraints I don't see how we could keep this.

The best counter-example is that sort function we were discussing earlier.

type Sort(slice []a:Lesser, less func(a:Lesser, a:Lesser)) { ... }

As you can see there's no nice way to make this happen, and the token-spam approaches to Generics start to sound better again. In order to define constraints on these we need to change two things from the original proposal:

  • There needs to be a way to point at a type argument and give it constraints.
  • The constraints need to last for longer than a single definition, perhaps that scope is a type, perhaps that scope is a file (file actually sounds pretty reasonable).

Disclaimer: The following isn't an actual amendment to the proposal because I'm just throwing random symbols out there, I'm just using these syntaxes as examples to illustrate what we could do to amend the proposal as it stands originally

// Decorator style, follows the definition of the type thorugh all
// of it's methods.
@a: Lesser, Hasher, Equaler
func Sort(slice []a) { ... }
@k: Equaler, Hasher
type HashTable k v struct

// Inline, follows the definition of the type through
// all of it's methods.
func [a: Hasher, Equaler] Sort(slice []a) { ... }
type [k: Hasher, Equaler] HashTable k v struct

// File-scope global style, if k appears as a generic argument
// it's constrained by this that appears at the top of the file underneath
// the imports but before any other code.
@k: Equaler, Hasher

Again note that none of the above I actually want to add to the proposal really. I'm just showing what sort of constructs we could use to solve the problem, and how they look is somewhat irrelevant right now.

The question we then need to answer is: Do we still gain value from the implicit generic arguments? The main point of the proposal was to keep the clean Go-like feel of the language, to keep things simple, to keep things sufficiently low noise by eliminating excessive tokens. In the many cases where there are no constraints necessary, for example a map function or the definition of a Result type, does it look good, does it feel like Go, is it useful? Assuming that constraints are also available in some form or another.

func map(slice []a, mapper func(a) b) {
  for i := range slice {
    slice[i] = mapper(slice[i])
  }
}

type Result a b struct {
  Ok  a
  Err b
}
@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 5, 2018

@aarondl I will have a go at explaining. The reason you need type constraints is because that is the only way you can call functions or methods on a type. consider the unconstrained type a what type can this be, well it could be a string or an Int or anything. So we cannot call any functions or methods on it because we do not know the type. We could use a type-switch and runtime reflection to get the type, and then call some functions or methods on it, but this is something we want to avoid with generics. When you constrain a type for example a is an Animal we can then call any method defined for an animal on a.

In your example, yes you can pass a mapper function in, but this is going to result in functions taking a lot of arguments, and is basically like a language with no interfaces, just first class functions. To pass every function you are going to use on type a is going to get very long list of functions in any real program, especially if you are writing mainly generic code for dependency-injection, which you want to do to minimise coupling.

For example what if the function that calls map is also generic? What if the function that calls that is generic etc. How do we define mapper if we don't yet know the type of a?

func m(slice []a) []b {
   mapper := func(x a) b {...}
   return map(slice, mapper)
}

What functions can we call on x when trying to define mapper?

keean commented Aug 5, 2018

@aarondl I will have a go at explaining. The reason you need type constraints is because that is the only way you can call functions or methods on a type. consider the unconstrained type a what type can this be, well it could be a string or an Int or anything. So we cannot call any functions or methods on it because we do not know the type. We could use a type-switch and runtime reflection to get the type, and then call some functions or methods on it, but this is something we want to avoid with generics. When you constrain a type for example a is an Animal we can then call any method defined for an animal on a.

In your example, yes you can pass a mapper function in, but this is going to result in functions taking a lot of arguments, and is basically like a language with no interfaces, just first class functions. To pass every function you are going to use on type a is going to get very long list of functions in any real program, especially if you are writing mainly generic code for dependency-injection, which you want to do to minimise coupling.

For example what if the function that calls map is also generic? What if the function that calls that is generic etc. How do we define mapper if we don't yet know the type of a?

func m(slice []a) []b {
   mapper := func(x a) b {...}
   return map(slice, mapper)
}

What functions can we call on x when trying to define mapper?

@aarondl

This comment has been minimized.

Show comment
Hide comment
@aarondl

aarondl Aug 5, 2018

@keean I understand the purpose and the function of the constraints. I simply don't value them as highly as simple things like generic container structs (not generic containers so to speak) and generic slices and therefore didn't even include them in the original proposal.

I still mostly believe that interfaces are the right answer to problems like the one you're talking about where you're doing dependency injection, that just simply doesn't seem to be the right place for generics but who am I to say. The overlap between their responsibilities is quite large in my eyes, hence why @Merovius and I had to have the discussion whether or not we could live without them, and he's pretty much got me convinced they'd be useful in some use cases hence I explored a little bit of what we might be able to do to add the feature to the proposal I originally made.

As for your example, you can call no functions on x. But you can still operate on the slice as any other slice which is tremendously useful on it's own. Also not sure what the func inside the func is... maybe you meant to assign to a var?

aarondl commented Aug 5, 2018

@keean I understand the purpose and the function of the constraints. I simply don't value them as highly as simple things like generic container structs (not generic containers so to speak) and generic slices and therefore didn't even include them in the original proposal.

I still mostly believe that interfaces are the right answer to problems like the one you're talking about where you're doing dependency injection, that just simply doesn't seem to be the right place for generics but who am I to say. The overlap between their responsibilities is quite large in my eyes, hence why @Merovius and I had to have the discussion whether or not we could live without them, and he's pretty much got me convinced they'd be useful in some use cases hence I explored a little bit of what we might be able to do to add the feature to the proposal I originally made.

As for your example, you can call no functions on x. But you can still operate on the slice as any other slice which is tremendously useful on it's own. Also not sure what the func inside the func is... maybe you meant to assign to a var?

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 6, 2018

@aarondl
Thanks, I fixed the syntax, however I think the meaning was still clear.

The examples I gave above used both parametric polymorphism and interfaces to achieve some level of generic programming, however the lack of multiple-dispatch is always going to place a ceiling on the level of generality achievable. As such it appears Go is not going to provide the features I am looking for in a language, that doesn't mean I can't use Go for some tasks, and infact I already am and it works well, even if I have had to cut-and-paste code that really only needs one definition. I just hope in the future if that code needs changing the developer can find all the pasted instances of it.

I am then in two minds as to whether the limited genarallity possible without such big changes to the language is a good idea, considering the complixity it will add. Maybe Go is better remaining simple, and people can add macro like pre-processing, or other languages that compile to Go, to provide these features? On the other hand, adding parametric polymorphism would be a good first step. Allowing those type parameters to be constrained would be a good next step. Then you could add associated type parameters to interfaces, and you would have something reasonably generic, but that's probably as far as you can get without multiple-dispatch. By splitting into separate smaller features I guess you would increase the chance of getting them accepted?

keean commented Aug 6, 2018

@aarondl
Thanks, I fixed the syntax, however I think the meaning was still clear.

The examples I gave above used both parametric polymorphism and interfaces to achieve some level of generic programming, however the lack of multiple-dispatch is always going to place a ceiling on the level of generality achievable. As such it appears Go is not going to provide the features I am looking for in a language, that doesn't mean I can't use Go for some tasks, and infact I already am and it works well, even if I have had to cut-and-paste code that really only needs one definition. I just hope in the future if that code needs changing the developer can find all the pasted instances of it.

I am then in two minds as to whether the limited genarallity possible without such big changes to the language is a good idea, considering the complixity it will add. Maybe Go is better remaining simple, and people can add macro like pre-processing, or other languages that compile to Go, to provide these features? On the other hand, adding parametric polymorphism would be a good first step. Allowing those type parameters to be constrained would be a good next step. Then you could add associated type parameters to interfaces, and you would have something reasonably generic, but that's probably as far as you can get without multiple-dispatch. By splitting into separate smaller features I guess you would increase the chance of getting them accepted?

@creker

This comment has been minimized.

Show comment
Hide comment
@creker

creker Aug 6, 2018

@keean
Is multiple-dispatch all that necessary? Very few languages natively support it. Even C++ doesn't support it. C# kinda supports it via dynamic but I've never used it in practice and the keyword in general is very very rare in real code. Examples I remember deal with something like JSON parsing, not writing generics.

creker commented Aug 6, 2018

@keean
Is multiple-dispatch all that necessary? Very few languages natively support it. Even C++ doesn't support it. C# kinda supports it via dynamic but I've never used it in practice and the keyword in general is very very rare in real code. Examples I remember deal with something like JSON parsing, not writing generics.

@sighoya

This comment has been minimized.

Show comment
Hide comment
@sighoya

sighoya Aug 6, 2018

Is multiple-dispatch all that necessary?

IMHO,I think @keean speaks about static multiple dispatch provided by typeclasses/interfaces.
This is even provided in C++ by method overloading (I don't know for C#)

What you mean is dynamic multiple dispatch which is quite cumbersome in static languages without union types. Dynamic languages circumvent this problem by omitting static type checking (partial type inference for dynamic languages, the same for C#'s "Dynamic" Type).

sighoya commented Aug 6, 2018

Is multiple-dispatch all that necessary?

IMHO,I think @keean speaks about static multiple dispatch provided by typeclasses/interfaces.
This is even provided in C++ by method overloading (I don't know for C#)

What you mean is dynamic multiple dispatch which is quite cumbersome in static languages without union types. Dynamic languages circumvent this problem by omitting static type checking (partial type inference for dynamic languages, the same for C#'s "Dynamic" Type).

@Inuart

This comment has been minimized.

Show comment
Hide comment
@Inuart

Inuart Aug 6, 2018

Could a type be provided as "just" a parameter?

func Append(t, t2 type, arr []t, value t2) []t {
    v := t(value) // conversion
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

Inuart commented Aug 6, 2018

Could a type be provided as "just" a parameter?

func Append(t, t2 type, arr []t, value t2) []t {
    v := t(value) // conversion
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)
@sighoya

This comment has been minimized.

Show comment
Hide comment
@sighoya

sighoya Aug 7, 2018

@Inuart wrote:

Could a type be provided as "just" a parameter?

Questionable to which degree this would be possible or desired in go

What you want could be achieved instead if generic constraints are supported:

func Append(arr []t, value s) []t  requires Convertible<s,t>{
    v := t(value) // conversion
    return append(arr, v)
}

var arr []int64
v := 0.5

arr = Append(arr, v)

Also this should be possible with constraints, too:

func convert(value s) t requires Convertible<s,t>{
    return t(value);
}

f:float64:=2.0

i:int64=convert(f)

sighoya commented Aug 7, 2018

@Inuart wrote:

Could a type be provided as "just" a parameter?

Questionable to which degree this would be possible or desired in go

What you want could be achieved instead if generic constraints are supported:

func Append(arr []t, value s) []t  requires Convertible<s,t>{
    v := t(value) // conversion
    return append(arr, v)
}

var arr []int64
v := 0.5

arr = Append(arr, v)

Also this should be possible with constraints, too:

func convert(value s) t requires Convertible<s,t>{
    return t(value);
}

f:float64:=2.0

i:int64=convert(f)

@golang golang deleted a comment from aaronlifton2 Aug 7, 2018

@andrewcmyers

This comment has been minimized.

Show comment
Hide comment
@andrewcmyers

andrewcmyers Aug 7, 2018

For what it is worth, our Genus language does support multiple dispatch. Models for a constraint can supply multiple implementations that are dispatched to.

andrewcmyers commented Aug 7, 2018

For what it is worth, our Genus language does support multiple dispatch. Models for a constraint can supply multiple implementations that are dispatched to.

@Inuart

This comment has been minimized.

Show comment
Hide comment
@Inuart

Inuart Aug 9, 2018

I understand that the Convertible<s,t> notation is needed for compile time safety, but could maybe be degraded to a runtime check

func Append(t, t2 type, arr []t, value t2) []t {
    v, ok := t(value) // conversion
    if !ok {
        panic(...) // or return an err
    }
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

But this looks more like syntax sugar for reflect.

Inuart commented Aug 9, 2018

I understand that the Convertible<s,t> notation is needed for compile time safety, but could maybe be degraded to a runtime check

func Append(t, t2 type, arr []t, value t2) []t {
    v, ok := t(value) // conversion
    if !ok {
        panic(...) // or return an err
    }
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

But this looks more like syntax sugar for reflect.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 9, 2018

@Inuart the point is the compiler can check the type implements the typeclass at compile time, so the runtime check is unnecessary. The benefit is better performance (so called zero cost abstraction). If it's a runtime check you may as well use reflect.

keean commented Aug 9, 2018

@Inuart the point is the compiler can check the type implements the typeclass at compile time, so the runtime check is unnecessary. The benefit is better performance (so called zero cost abstraction). If it's a runtime check you may as well use reflect.

@keean

This comment has been minimized.

Show comment
Hide comment
@keean

keean Aug 9, 2018

@creker

Is multiple-dispatch all that necessary?

I am in too minds about this. On the one hand multiple-dispatch (with mutli-parameter type classes) do not work well with existentials, what 'Go' calls 'interface values'.

type Equals<T> interface {eq(right T) bool}
(left I) eq(right I) bool {return left == right}
(left I) eq(right F) bool {return false}
(left F) eq(right I) bool {return false}
(left F) eq(right F) bool {return left == right}

func main() {
    x := []Equals<?>{I{2}, F{4.0}, I{2}, F{4.0}}
}

We cannot define the slice of Equals because we have no way to indicate the right hand parameter is from the same collection. We cannot even do this in Haskell:

data Equals = forall a . IEquals a a => Equals a

This is no good because it only allows a type to be compared with itself

data Equals = forall a b . IEquals a b => Equals a

This is no good because we have no way to constrain b to be another existential in the same collection as a (if a even is in a collection).

It does however make it very easy to extend with a new type:

(left K) eq(right I) bool {return false}
(left K) eq(right F) bool {return false}
(left I) eq(right K) bool {return false}
(left F) eq(right K) bool {return false}
(left K) eq(right K) bool {return left == right}

And this would be even more concise with default instances or specialisation.

On the other hand we can rewrite this in 'Go' that works right now:

package main

type I struct {v int}
type F struct {v float32}

type EqualsInt interface {eqInt(left I) bool}
func (right I) eqInt (left I) bool {return left == right}
func (right F) eqInt (left I) bool {return false}

type EqualsFloat interface {eqFloat(left F) bool}
func (right I) eqFloat (left F) bool {return false}
func (right F) eqFloat (left F) bool {return left == right}

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

type EqualsLeft interface {eq(right EqualsRight) bool}
func (left I) eq (right EqualsRight) bool {return right.eqInt(left)}
func (left F) eq (right EqualsRight) bool {return right.eqFloat(left)}

type Equals interface {
    EqualsLeft
    EqualsRight
}

func main() {
    x := []Equals{I{2}, F{4.0}, I{2}, F{4.0}}
    println(x[0].eq(x[1]))
    println(x[1].eq(x[0]))
    println(x[0].eq(x[2]))
    println(x[1].eq(x[3]))
}

This works nicely with the existential (interface value), however its much more complex, harder to see what is going on and how it works, and it has the big restriction that we need one interface per type and we need to hard code the acceptable right hand side types like this:

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

Which means we would have to modify the library source to add a new type because the interface EqualsRight is not extensible.

So without multi-parameter interfaces we cannot define extensible generic operators like equality. With multi-parameter interfaces existentials (interface values) become problematic.

keean commented Aug 9, 2018

@creker

Is multiple-dispatch all that necessary?

I am in too minds about this. On the one hand multiple-dispatch (with mutli-parameter type classes) do not work well with existentials, what 'Go' calls 'interface values'.

type Equals<T> interface {eq(right T) bool}
(left I) eq(right I) bool {return left == right}
(left I) eq(right F) bool {return false}
(left F) eq(right I) bool {return false}
(left F) eq(right F) bool {return left == right}

func main() {
    x := []Equals<?>{I{2}, F{4.0}, I{2}, F{4.0}}
}

We cannot define the slice of Equals because we have no way to indicate the right hand parameter is from the same collection. We cannot even do this in Haskell:

data Equals = forall a . IEquals a a => Equals a

This is no good because it only allows a type to be compared with itself

data Equals = forall a b . IEquals a b => Equals a

This is no good because we have no way to constrain b to be another existential in the same collection as a (if a even is in a collection).

It does however make it very easy to extend with a new type:

(left K) eq(right I) bool {return false}
(left K) eq(right F) bool {return false}
(left I) eq(right K) bool {return false}
(left F) eq(right K) bool {return false}
(left K) eq(right K) bool {return left == right}

And this would be even more concise with default instances or specialisation.

On the other hand we can rewrite this in 'Go' that works right now:

package main

type I struct {v int}
type F struct {v float32}

type EqualsInt interface {eqInt(left I) bool}
func (right I) eqInt (left I) bool {return left == right}
func (right F) eqInt (left I) bool {return false}

type EqualsFloat interface {eqFloat(left F) bool}
func (right I) eqFloat (left F) bool {return false}
func (right F) eqFloat (left F) bool {return left == right}

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

type EqualsLeft interface {eq(right EqualsRight) bool}
func (left I) eq (right EqualsRight) bool {return right.eqInt(left)}
func (left F) eq (right EqualsRight) bool {return right.eqFloat(left)}

type Equals interface {
    EqualsLeft
    EqualsRight
}

func main() {
    x := []Equals{I{2}, F{4.0}, I{2}, F{4.0}}
    println(x[0].eq(x[1]))
    println(x[1].eq(x[0]))
    println(x[0].eq(x[2]))
    println(x[1].eq(x[3]))
}

This works nicely with the existential (interface value), however its much more complex, harder to see what is going on and how it works, and it has the big restriction that we need one interface per type and we need to hard code the acceptable right hand side types like this:

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

Which means we would have to modify the library source to add a new type because the interface EqualsRight is not extensible.

So without multi-parameter interfaces we cannot define extensible generic operators like equality. With multi-parameter interfaces existentials (interface values) become problematic.

@deanveloper

This comment has been minimized.

Show comment
Hide comment
@deanveloper

deanveloper Aug 15, 2018

My main issue with a lot of the proposed syntaxes (syntaces?) Blah[E] is that the underlying type does not show any information about containing generics.

For instance:

type Comparer[C] interface {
    Compare(other C) bool
}
// or
type Comparer c interface {
    Compare(other c) bool
}
...

This means we are declaring a new type which adds more information onto the underlying type. Isn't the point of the type declaration to define a name based on another type?

I'd propose a syntax more along the line of

type Comparer interface[C] {
    Compare(other C) bool
}

This means that really Comparer is just a type based on interface[C] { ... }, and interface[C] { ... } is of course it's own separate type from interface { ... }. This allows you to use a generic interface without naming it, if you want (which is allowed with normal interfaces). I think this solution is a bit more intuitive and works well with Go's type system, although please correct me if I am wrong.

Note: Declaring a generic type would only be allowable on interfaces, structs, and funcs with the following syntaxes:
interface[G] { ... }
struct[G] { ... }
func[G] (vars...) { ... }

Then "implementing" the generics would have the following syntaxes:
interface[G] { ... }[string]
struct[G] { ... }[string]
func[G] (vars...) { ... }[int](args...)

And with some examples to make it a bit more clear:

Interfaces

package add

type Adder interface[E] {
    // Adds the element and returns the size
    Add(elem E) int
}

// Adds the integer 5 to any implementation of Adder[int].
func AddFiveTo(a Adder[int]) int {
    return a.Add(5)
}

Structs

package heap

type List struct[T] {
    slice []T
}

func (l *List) Add(elem T) { // T is a type defined by the receiver
    l.slice = append(l.slice, elem)
}

Functions

func[A] AddManyTo(a Adder[A], many ...A) {
    for _, each := range a {
        a.Add(each)
    }
}

deanveloper commented Aug 15, 2018

My main issue with a lot of the proposed syntaxes (syntaces?) Blah[E] is that the underlying type does not show any information about containing generics.

For instance:

type Comparer[C] interface {
    Compare(other C) bool
}
// or
type Comparer c interface {
    Compare(other c) bool
}
...

This means we are declaring a new type which adds more information onto the underlying type. Isn't the point of the type declaration to define a name based on another type?

I'd propose a syntax more along the line of

type Comparer interface[C] {
    Compare(other C) bool
}

This means that really Comparer is just a type based on interface[C] { ... }, and interface[C] { ... } is of course it's own separate type from interface { ... }. This allows you to use a generic interface without naming it, if you want (which is allowed with normal interfaces). I think this solution is a bit more intuitive and works well with Go's type system, although please correct me if I am wrong.

Note: Declaring a generic type would only be allowable on interfaces, structs, and funcs with the following syntaxes:
interface[G] { ... }
struct[G] { ... }
func[G] (vars...) { ... }

Then "implementing" the generics would have the following syntaxes:
interface[G] { ... }[string]
struct[G] { ... }[string]
func[G] (vars...) { ... }[int](args...)

And with some examples to make it a bit more clear:

Interfaces

package add

type Adder interface[E] {
    // Adds the element and returns the size
    Add(elem E) int
}

// Adds the integer 5 to any implementation of Adder[int].
func AddFiveTo(a Adder[int]) int {
    return a.Add(5)
}

Structs

package heap

type List struct[T] {
    slice []T
}

func (l *List) Add(elem T) { // T is a type defined by the receiver
    l.slice = append(l.slice, elem)
}

Functions

func[A] AddManyTo(a Adder[A], many ...A) {
    for _, each := range a {
        a.Add(each)
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment