Skip to content
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: allow type parameters in methods #49085

Open
mariomac opened this issue Oct 20, 2021 · 294 comments
Open

proposal: spec: allow type parameters in methods #49085

mariomac opened this issue Oct 20, 2021 · 294 comments
Labels
generics Issue is related to generics LanguageChange Proposal Proposal-Hold v2 A language change or incompatible library change
Milestone

Comments

@mariomac
Copy link

mariomac commented Oct 20, 2021

According to the Type parameters proposal, it is not allowed to define type parameters in methods.

This limitation prevents to define functional-like stream processing primitives, e.g.:

func (si *stream[IN]) Map[OUT any](f func(IN) OUT) stream[OUT]

While I agree that these functional streams might be unefficient and Go is not designed to cover this kind of use cases, I would like to emphasize that Go adoption in stream processing pipelines (e.g. Kafka) is a fact. Allowing type parameters in methods would allow constructing DSLs that would greatly simplify some existing use cases.

Other potential use cases that would benefit from type paremeters in methods:

  • DSLs for testing: Assert(actual).ToBe(expected)
  • DSLs for mocking: On(obj.Sum).WithArgs(7, 8).ThenReturn(15)

Edited by @ianlancetaylor to add: for a summary of why this has not been approved, please see https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#no-parameterized-methods .

@gopherbot gopherbot added this to the Proposal milestone Oct 20, 2021
@fzipp
Copy link
Contributor

fzipp commented Oct 20, 2021

The document also explains what the problems are. So what are your solutions to these?

@go101
Copy link

go101 commented Oct 20, 2021

This proposal is good to define an io.ImmutableWriter {Write(data byteview)(int, error)} interface:
https://github.com/go101/go101/wiki/A-proposal-to-avoid-duplicating-underlying-bytes-when-using-strings-as-read-only-%5B%5Dbyte-arguments

@ianlancetaylor ianlancetaylor changed the title Proposal: Allow type parameters in methods proposal: spec: allow type parameters in methods Oct 20, 2021
@ianlancetaylor
Copy link
Contributor

This proposal is a non-starter unless someone can explain how to implement it.

@ianlancetaylor ianlancetaylor added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Oct 20, 2021
@deanveloper
Copy link

deanveloper commented Oct 20, 2021

@ianlancetaylor from the generics proposal

Or, we could decide that parameterized methods do not, in fact, implement interfaces, but then it's much less clear why we need methods at all. If we disregard interfaces, any parameterized method can be implemented as a parameterized function.

I think this solution makes the most sense. They could then (under the hood) be treated a regular function. The reason why this would be useful is that methods do not only serve the purpose of implementing interfaces; methods also serve as a means of organization for functions that operate on particular structures.

It may be a bit of a challenge about how type-parameterized methods would appear in "reflect", though.

@go101
Copy link

go101 commented Oct 21, 2021

The problem would be simpler if the parameter type possibility set is known at compile time,

It may be a bit of a challenge about how type-parameterized methods would appear in "reflect", though.

One new problem I'm aware of is there might be many methods have the same name for a certain type.
So the Type.MethodByName might return a slice value (assume the parameter type possibility set is known at compile time).
Any other new problems?

@seankhliao seankhliao added the generics Issue is related to generics label Oct 21, 2021
@AndrewHarrisSPU
Copy link

AndrewHarrisSPU commented Oct 21, 2021

One new problem I'm aware of is there might be many methods have the same name for a certain type.

If we're ultimately talking about multiple dispatch, the languages that really cater towards this do a massive amount of overloading. One language I find fun and interesting with this style is Julia, where things like + or * or show have hundreds of overloads when booting the REPL. From a software engineering perspective, there are tradeoffs - I absolutely trust Go to compile long into the future, and to have fewer surprises about packages ... Remarkably and IMHO related to robustness, Go doesn't have programmers defining function overloads in source code - I'm not convinced generics should change this.

Particularly for stream-to-stream conversion, I do think that the List Transform example is useful. We have to provide a concrete T1 -> T2 conversion function, but in a sense we have to figure out how to convert T1 to T2 in any kind of system.

I think it's also often possible to have a higher-order function that generates conversion functions, while more specialized conversion functions are also naturally expressible in Go. Example: a very generic color conversion API might specify an interface with ToRGB() and FromRGB() methods, and this can go pretty far. We can express 8-bit to 16-bit RGB conversion here through the interfaces, the same as e.g. HSV or LAB conversions, but there's a faster bit-shifting path. With a sense of a generic default, something like bufio.Scanner seems plausible - where the default just works, but we can optionally provide a better color conversion the same way we can provide a different SplitFunc.

@DeedleFake
Copy link

DeedleFake commented Oct 21, 2021

@deanveloper

Even just that would allow for, for example, an iterator implementation, though it does require wrapping it in another type because of the lack of extension functions:

type Nexter[T any] interface {
  Next() (T, bool)
}

type NextFunc[T any] func() (T, bool)

func (n NextFunc[T]) Next() (T, bool) {
  return n()
}

type Iter[T any, N Nexter[T]] struct {
  next N
}

func New[T any, N Nexter[T]](next) Iter[T, N] {
  return Iter[T, N]{next: next}
}

func (iter Iter[T, N]) Map[R any](f func(T) R) Iter[R, NextFunc[R]] {
  return New(NextFunc[R](func() (r R, ok bool) {
    v, ok := iter.next.Next()
    if !ok {
      return r, false
    }
    return f(v), true
  })
}

// And so on.

Usage is still awkward without a short-form function literal syntax, though, unfortunately:

s := someIter.Filter(func(v int) bool { return v > 0 }).Map(func(v int) string { return strconv.FormatInt(v, 10) }).Slice()
// vs.
s := someIter.Filter(func(v) -> v > 0).Map(func(v) -> strconv.FormatInt(v, 10)).Slice()

@batara666
Copy link

This will add so much complexity

@deanveloper
Copy link

@batara666 can you explain why? adding type parameters to methods doesn’t seem like it’d add that much complexity to me.

@Merovius
Copy link
Contributor

Merovius commented Oct 27, 2021

I think before we can think about if and how to do this, we should first address the "no higher level abstraction" restriction of generics, i.e. the inability to pass around a generic type/function without instantiation. The reason is that if we allowed additional type parameters on methods, we'd also de-facto allow to pass around generic functions without instantiation:

type F struct{}

func (F) Call[T any] (v T) { /* … */ }

func main() {
    var f F // f is now de-facto an uninstantiated func[T any](T)
}

Therefore, to allow additional type-parameters on methods, we also have to answer how to pass around uninstantiated generic functions.

Moreover, if we'd allow passing around uninstantiated generic types/functions, we could already build the abstractions given as motivations in the proposal-text.

So, given that solving "no higher level abstraction" is a strictly easier problem to solve, while providing most of the benefit of solving "no additional type parameters on methods", it seems reasonable to block the latter on solving the former.

Lastly, I'd urge everyone to consider that these limitations where not left in the generics design by accident. If they where really that easy to solve, the solution would have been in the design to begin with. It will take some time to solve them.

@ianlancetaylor ianlancetaylor removed the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Nov 3, 2021
@go101
Copy link

go101 commented Nov 12, 2021

The "type" concept almost means a memory layout.
If we could use the 1.18 constraint concept as general types,
then many problems will be solved.

A value of a subset type could be passed/assigned to a superset type.
A function with a superset type parameter could be used as a function with a subset type parameter.

For Go, the change would be too large.
It is best to experiment the idea on a new language.

@AndrewHarrisSPU
Copy link

I was (far too) pleased (with myself) when I figured out this is possible: https://gotipplay.golang.org/p/1ixYAwxwVss

The part about lifting values into type system 'symbols' feels like a bit DIY and tricky, not sure there isn't something better here. I did feel like the dispatch() call is technically interesting. Inferring from the function/method supplied to dispatch() wasn't obvious to me at first. Without overloading methods, just different instantiations of the dispatch() function, it is plausible to arrive at the correct dispatch over a set of concrete implementations.

@ianlancetaylor
Copy link
Contributor

I'm putting this proposal on hold until we have more familiarity with the current generics implementation.

@mariomac
Copy link
Author

mariomac commented Dec 4, 2021

Playing with a new library that intensively uses generics, I provided an equivalence implementation between a Method and a Function:

https://github.com/mariomac/gostream/blob/bf84997953f02b94e28da0d6c4d38585d2677df2/stream/str_to_str.go#L5-L14

At the end, the difference is only where the parameter is placed (as a receiver, or as a first argument), but the function allows you map to a Stream of different type and with the method you can only generate streams of the same type.

With the code from the above link, I verified that this compiles:

type Mapper[IT, OT any] func(Stream[IT], func(IT)OT) Stream[OT]
var _ Mapper[int, float64] = Map[int, float64]

Simplifying, and obviating some internals from Go that I might ignore, I could see the generic Mapper type as a "single-method generic interface", that is implemented by the Map function, and it can be instantiated into a Mapper instance with concrete types.

In order to overcome the No parametrized methods issue pointed by @fzipp, from my partial view, I think that the example issue can be approached the same way as Java does: using interface{} behind the scenes and panic if the customer did a bad assignment (also the compiler could warn about the unsafe operation). Then for example the code from the example:

func CheckIdentity(v interface{}) {
	if vi, ok := v.(p2.HasIdentity); ok {
		if got := vi.Identity[int](0); got != 0 {
			panic(got)
		}
	}

Would be translated to something equivalent to:

func CheckIdentity(v interface{}) {
	if vi, ok := v.(p2.HasIdentity); ok {
		if got := vi.Identity(0).(int); got != 0 {
			panic(got)
		}
	}

Then the third line would panic if the v interface does not implement Identity[int]. The same way that Go does currently when you try to cast an identity{} reference to a wrong type.

In this case, we are translating the error check from the compile time to the runtime, but anyway this is what we actually have now if lack of parametrized methods forces us to continue using unsafe type castings.

@ianlancetaylor
Copy link
Contributor

In your rewrite of CheckIdentity what do we gain by using a type parameter with the method? If the code is not type checked at compile time, we can just return an interface type, which already works today.

@deanveloper
Copy link

deanveloper commented Dec 4, 2021

I think that the example issue can be approached the same way as Java does: using interface{} behind the scenes and panic if the customer did a bad assignment

This is a bad idea in my opinion - Type erasure is one of the most annoying limitations of generics in Java. I think it would go against the grain of the simplicity that Go aims for.

@mccolljr
Copy link

mccolljr commented Dec 7, 2021

I don't really have a horse in this race, but I find this proposal interesting and wanted to put down my thoughts.

Based on what I see here: https://go.godbolt.org/z/1fz9s5W8x
This:

func (x *SomeType) Blah() { /* ... */ }

And this:

func Blah(x *SomeType) { /* ... */ }

compile to nearly identical code.

If we have a type S:

type S struct {
    /* ... */
}

...and S has a method DoThing with a type parameter T:

func (s S) DoThing[T any](arg T) { /* ... */ }

...then we effectively have a generic function with the signature:

func DoThing[T any](s S, arg  T) { /* ... */ }

Of course, if we have a generic type G:

type G[T any] struct {
    /* ... */
}

...and G has a method DoStuff with a type parameter U:

func (g G[T]) DoStuff[U any](arg U) { /* ... */ }

...then we effectively have a generic function with the signature:

func DoStuff[T, U any](g G[T], arg U) { /* ... */ }

In order to use either of these "functions", all of the type parameters have to be known.

That means that in the case of S, the only way to refer to S.DoThing is to instantiate it: S.DoThing[int], S.DoThing[float64], etc.
The same is true for G, with the additional requirement that G is also instantiated: G[int].DoThing[float64], etc.

Within this limited context, it seems to me like it wouldn't be a huge leap to allow type parameters on methods - it ends up referring to what is essentially a generic function, and we know things about generic functions:

  1. They can't be used unless all type parameters are known statically at compile time, and
  2. Each unique instantiation results in a unique function, semantically speaking (the actual implementation of course may choose to use a single function and internally use type switching, etc, etc)

The mechanism by which this interacts with interface definitions/implementations is less clear to me. Though, I think it is reasonable to say that a generic interface can't be implemented directly - it must be instantiated first. I'm not as sure of this, but it seems that it might also be true that an interface can only be implemented by a fully instantiated type.

Even in code like this:

type GenericInterface[T any] interface {
    Foo() T
}

type GenericStruct[T any] struct {
    Bar T
}

func (g GenericStruct[T]) Foo() T {
    return g.Bar
}

func MakeGeneric[U any]() GenericInterface[U] {
    return GenericStruct[U]{}
}

It seems like, within the context of MakeGeneric[T], we could consider both GenericInterface[T] and GenericStruct[T] to be instantiated with the some specific type T, which is the type value given to the type parameter U in MakeGeneric[U]. The determination that GenericStruct[T] implements GenericInterface[T] in this context is different from making a general statement that "GenericStruct[T] implements GenericInterface[T] for all T", which is what I would think of as "implementation without instantiation"

One area that seems complex is interfaces whose methods have type parameters.

For example, if we had:

type Mappable[T any] interface {
    Map[U any](func(T) U) []U
}

What would it mean to "instantiate" Mappable[T]?
Can you use a type assertion such as blah.(Mappable[int]?
If Mappable[T].Map had the signature Map[U comparable](func(T) U) []U, would a type with a method
Map[U any](func(T) U) []U be treated as implementing Mappable[T]?
This kind of interface seems to introduce a lot of ambiguity that would be difficult to resolve in a satisfactory manner.

It seems much simpler to disallow that kind of interface entirely, and just require something like:

type Mappable[T, U any] interface {
    Map(func(T) U) []U
}

I think that could still be just as useful, depending on how interface implementation is handled when the underlying type has methods with generic parameters.

As an example:

// Slice[T] provides slice operations over a slice of T values
type Slice[T any] []T

// Map[U] maps a Slice[T] to a Slice[U]
func (s Slice[T]) Map[U any](func (T) U) Slice[U]

type Mappable[T, U any] {
    Map(func (T) U) Slice[U]
}

// In order for this assignment to be valid:
// 1. Slice[int] must have a method named Map ✅
// 2. Slice[int].Map must have the same number of arguments ✅
// 3. Slice[int].Map must have the same number of returns ✅
// 4. Slice[int].Map must have the same types for each argument and return ???
var _ Mappable[int, float64] = Slice[int]{1,2,3}

It seems reasonable to me to say that Slice[int] implements Mappable[int, float64], since the method Map on Slice[int] can be instantiated & called with a U set to float64.

In this case, assuming that methods with type parameters are allowed, I would think the compiler could do something like:

  1. Notice that Mappable[int, flloat64] is implemented for Slice[Int] when Slice[int].Map is insantiated with float64
  2. Generate the code for that instantiation of Slice[int].Map, and
  3. Use the pointer to that particular instantiation of Slice[int].Map in the vtable

If you're calling the method an the interface object, then you only have access to that one particular instantiation of
the Slice[int].Map method. If use a type assertion to get back the original Slice[int] type, then you can of course call any number of Map variants on it, because the compiler knows what the concrete type is again.

To summarize:

Given that it is a feature of go that interface implementation can be tested at runtime via type assertions, reflection, etc, I don't see any way around banning generic methods on interface definitions. However, because methods are more or less sugar for functions, it seems to me it would be possible to allow generic parameters on methods of concrete types, and to allow these methods to participate in implementation of fully instantiated interface types.

@Merovius
Copy link
Contributor

Merovius commented Dec 7, 2021

Given that it is a feature of go that interface implementation can be tested at runtime via type assertions, reflection, etc, I don't see any way around banning generic methods on interface definitions. However, because methods are more or less sugar for functions, it seems to me it would be possible to allow generic parameters on methods of concrete types, and to allow these methods to participate in implementation of fully instantiated interface types.

I don't think you are solving the problems from the design doc, though:

type IntFooer interface { Foo() int }
type StringFooer interface { Foo() string }
type X struct{}
func (X) Foo[T any]() T { return *new(T) }

func main() {
    var x X
    x.(StringFooer) // How does this work? Note that we can't use runtime code generation
    reflect.ValueOf(x).MethodByName("Foo").Type() // What is this type?
}

This fulfills all your criteria, it uses no parameterized interfaces and X is a concrete (non-parameterized) type. In particular, answering these questions here is the minimum required to make this feature useful.

It is very easy to look at the proposal text and think "this would be a useful feature to have, I obviously would like it in the language". But because it's such an obvious feature to put in, it would be great if people ask themselves why the Go team didn't put it in in the first place. Because there are reasons and these reasons need answering.

@alvaroloes
Copy link

I would like to drop an idea here which I think can be useful for the "type parameters in methods" topic. Maybe it has an obvious flaw I haven't seen or it has already been considered or discussed, but I couldn't find anything about it. Please, let me know if so.

With the current proposal, we can't have type parameters in methods but, couldn't we achieve the same effect if we put the type parameters in the type definition?

I mean, instead of doing this:

type Slice[T any] []T

func (s Slice[T]) Map[U any](func (T) U) Slice[U]

Do this (move the U type parameter from the method "Map" to the struct):

type Slice[T any, U any] []T

func (s Slice[T,U]) Map(func (T) U) Slice[U]

@Merovius Wouldn't this solve the issue you mentioned in the above comment? Your example would end up like this:

type IntFooer interface { Foo() int }
type StringFooer interface { Foo() string }
type X[T any] struct{}
func (X) Foo() T { return *new(T) }

func main() {
    var x X[int] // You are forced to specify the type parameter here with the current proposal. I guess it could be inferred it this were an initialization instead of a declaration only
    x.(StringFooer) // This would fail, as it doesn't conform to the interface
    x.(IntFooer) // This would pass
    reflect.ValueOf(x).MethodByName("Foo").Type() // "Foo(X) int"
}

I think this would work with the current proposal without changes.

As I said, I could be missing something obvious here. Let me know if that's the case.

@Merovius
Copy link
Contributor

Merovius commented Dec 7, 2021

@alvaroloes

couldn't we achieve the same effect if we put the type parameters in the type definition?

You can easily do this, but it's not the same effect. People who want this specifically want the type-parameter of the method to be independent of the type itself, to implement higher-level abstractions.

@alvaroloes
Copy link

I see, thanks. After thinking it twice, I now see that what I propose would be very limiting as, after instantiating the type, you could not call the method with different type parameters (for example, call "map" with a function that return strings one time and then another time with a function that return ints).

All right, I know there was something obvious here. Thanks for the response!

@mccolljr
Copy link

mccolljr commented Dec 7, 2021

I don't think you are solving the problems from the design doc, though:

I'm sure that's true, I probably would benefit from reviewing it again. To be fair, though, I wasn't trying to put together a concrete proposal - I understand why this is a complex topic and why it isn't in the first pass at generics, and why it may never be added to the language. I don't have a horse in this race beyond the fact that I find this interesting. My intent was to think out loud about what restrictions might make this more concretely approachable. Also, for what it's worth, I think that disallowing parameterized methods on interface types could be seen to addresses some of the problems put forth in the proposal.

type IntFooer interface { Foo() int }
type StringFooer interface { Foo() string }
type X struct{}
func (X) Foo[T any]() T { return *new(T) }

func main() {
    var x X
    x.(StringFooer) // How does this work? Note that we can't use runtime code generation
    reflect.ValueOf(x).MethodByName("Foo").Type() // What is this type?
}

[...] answering these questions here is the minimum required to make this feature useful.

I do not disagree. I feel as if you may have misinterpreted my intent. I am not saying "this is so easy, look at how we can do it" - I am saying "here is an interesting constraint that might make this more approachable, and which could perhaps be used as the basis for additional discussion"

I'm willing to brainstorm this, but again I am not proposing a concrete solution as much as attempting to provide a possible set of constraints for discussion. If that exercise shows that thia feature would too complex, that is a totally acceptable outcome in my opinion.

Obviously we cannot use runtime code generation, I don't recall proposing that nor do I think it is necessitated by anything said above. Given that, here are some possible (not exhaustive or comprehensive) directions the compiler could choose:

For x.(StringFooer)

  • The program could encode the types that a generic method/function/etc has been instantiated with. This would allow x.(StringFooer) to correctly select the implementation of Foo that applies. Of course, if X.Foo is never explicitly instantiated with string, then it could be surprising to a user that this fails. Perhaps the cost of that potential confusion is unacceptable. This failure could of course be solved by adding var _ StringFooer = X{} somewhere in the code, and perhaps the panic message could indicate that the failure was due to uninstantiated generic methods rather than uninstantiated

  • The compiler could generate a fallback implementation using interface{} or some minimum interface that it can use in these situations. In the case of type sets, the fallback could use a type switch. Perhaps if type switching on ~ types is implemented this becomes easier.

For reflect

  • Similar to above, the compiler could generate metadata about which instantiation were generated and this could be introspectable from reflect. A public IsGeneric flag could be added to the descriptor for methods and calls to method values could validate the given types against the instantiation list to verify the proper code had been generated.

  • Similar to above, the compiler could simply generate fallback implementations for generic methods (functions etc).

It is very easy to look at the proposal text and think "this would be a useful feature to have, I obviously would like it in the language". But because it's such an obvious feature to put in, it would be great if people ask themselves why the Go team didn't put it in in the first place. Because there are reasons and these reasons need answering.

This is exactly what I attempted to do here. I wrote this at 1am on the last legs of a cup of coffee, and it seems I failed to consider some scenarios in my comment. A simple "How would this address X and Y" would have accomplished the same effect without this lecture at the end.

@Merovius
Copy link
Contributor

Merovius commented Dec 7, 2021

To be clear, this is what the proposal says about this question:

We could instantiate it at link time, but in the general case that requires the linker to traverse the complete call graph of the program to determine the set of types that might be passed to CheckIdentity. And even that traversal is not sufficient in the general case when type reflection gets involved, as reflection might look up methods based on strings input by the user. So in general instantiating parameterized methods in the linker might require instantiating every parameterized method for every possible type argument, which seems untenable.

Or, we could instantiate it at run time. In general this means using some sort of JIT, or compiling the code to use some sort of reflection based approach. Either approach would be very complex to implement, and would be surprisingly slow at run time.

Or, we could decide that parameterized methods do not, in fact, implement interfaces, but then it's much less clear why we need methods at all. If we disregard interfaces, any parameterized method can be implemented as a parameterized function.

So while parameterized methods seem clearly useful at first glance, we would have to decide what they mean and how to implement that.

The solution you suggest seem a variation on the second option here.

@nyanpassu
Copy link

nyanpassu commented Jan 11, 2024

@nyanpassu You keep posting longer and longer comments. But you are still not explaining clearly what it is you are actually proposing. At least not in a way I can understand. So let me repeat my question from above: If what you suggests works and does everything we want, why would we introduce a second concept, instead of just making it the one and only form of generics in the language?

Because it's different approaches in computer languages.

Generics generally have two major approaches in computer languages, first kind is generate different codes for different types at compile time, this is adhoc-polymorphism(althrough it will appear in the form of parametric polymorphism). This kind of approach includes C++, Rust, and Golang.

Another approach is type erasure, which generates single implementation for different types. Java is a typical example of this kind of approach(before project Valhalla, which will help Java programmer generate different codes for different types). My proposal is about how we can use the same approach in Golang, meanwhile not to introduce breaking changes(still can generate different codes for different types under other context).

The problem for us to implement generic method of interface is that, the actual type of the value behind interface is dynamic at runtime, and you can't infer the type at compile time(or at lease very hard to). The documents of Golang generic proposal has already given a statement about that.

So we have two option, one thing is restrinct the type parameter boundaries like this:

type TypeSet interface {
  int, string, bool
}

interface Foobar {
  Foo[T TypeSet]()
}

So we we cast a given type to Foobar, we should generate specilized code for every type in the type set. So Golang runtime can link the type and implementation at runtime. Without the type set, then you have to generate specilized code for every type in the program and it's clearly impossible and wasting(And actually, we can just create an algebraic data type or type enum for this approach).

But the problem of this approach has already been state in the proposal too, that is interface of Golang is structural but not by declaration. By declaration is the kind of implementation of the interface of Java, the trait of Rust, the type class of Haskell and Scala. You have to declare some type is the kind of some interface.

class Foo implements FooBar {
  ...
}

FooBar foo = new Foo();

Golang interface subtyping is structural and dynamical, you can do this in Golang.

package p1

type Foo struct {} 

func (Foo) FooBar() {}

package p2

interface FooBar { FooBar() }

package main

import (
  "p1"
  "p2"
)

func check(val any) {
  if v, ok := val.(p2.FooBar); ok {
   ...
  }
}

func test(f func(any)) {
  f(p1.Foo{})
}

func main() {
  test(check)
}

It's very hard for us to infer that Foo implements FooBar, you need do a full calculation of expressions of all the program at compile time to do that. So using a type set to restrict type boundaries is not practical in Golang. So the approach of type erasure like Java is a practical way to allow interface generic method.

@Merovius
Copy link
Contributor

Merovius commented Jan 11, 2024

@nyanpassu So, AIUI, all the arguments against requiring Go generics to be implemented using type erasure apply to your suggestion as well. We won't suddenly be okay with requiring a specific implementation strategy for generics, just because it is only needed for a subset of code.

Also, the idea of having two implementations for generics has already been discussed and rejected above.

So it does not seem to me like you are adding anything new to the discussion. You can't take two bad ideas and put both into the language and have the result be something good.

@nyanpassu
Copy link

nyanpassu commented Jan 11, 2024

@nyanpassu So, AIUI, all the arguments against requiring Go generics to be implemented using type erasure apply to your suggestion as well. We won't suddenly be okay with requiring a specific implementation strategy for generics, just because it is only needed for a subset of code.

Also, the idea of having two implementations for generics has already been discussed and rejected above.

So it does not seem to me like you are adding anything new to the discussion. You can't take two bad ideas and put both into the language and have the result be something good.

Yes, type erasure is not something new. And the idea of two implementations for generics coexists have been discussed in the previous posts. But my proposal is different. My proposal is not a universal solution for generics, but a specical solution for cases which needs parametric polymorphism. And it's a solution that user can decide which to use.

If a solution on interface generic method can't be reach for now. Can we have generic method for types first? It's really useful in many projects. Currently I have to code in function

func Fork[T any](s Scope, f func() T) func() T {
   ...
}

And a api caller have to write

s := Scope{}
future1 := Fork(s, func() int { ... })
future2 := Fork(s, func() bool { ... })

But not

s := Scope{}
future1 := s.Fork(func() int { ... })
future2 := s.Fork(func() bool { ... })

Which is more friendly to api caller.

@Merovius
Copy link
Contributor

My proposal is not a universal solution for generics, but a specical solution for cases which needs parametric polymorphism.

Please read what I said. Your solution requires to be implemented using type erasure. We do not want to require a specific implementation strategy. Saying "yes, you have to use a specific implementation strategy, but it will then also only be used for special cases" makes it strictly worse. It is also what has been rejected above.

I would prefer if this already long issue would not be made longer by continuing to re-state ideas that have already been rejected.

Can we have generic method for types first?

That too has been discussed above and rejected - several times now.

@nyanpassu
Copy link

nyanpassu commented Jan 11, 2024

My proposal is not a universal solution for generics, but a specical solution for cases which needs parametric polymorphism.

Please read what I said. Your solution requires to be implemented using type erasure. We do not want to require a specific implementation strategy. Saying "yes, you have to use a specific implementation strategy, but it will then also only be used for special cases" makes it strictly worse. It is also what has been rejected above.

I would prefer if this already long issue would not be made longer by continuing to re-state ideas that have already been rejected.

Can we have generic method for types first?

That too has been discussed above and rejected - several times now.

My proposal is not about the special cases discussed there. When I talk about "a special solution for cases which needs parametric polymorphism", the special relates to parametric polymorphism. No need to analyze the code user written, no need to worry user are using reflect or not. And it even works on plugins. It's not some special cases need to be dealt with when some special code paths are encountered, should not be hard to maintain.

As I see the problem could only be solved in three ways: type level programming(type level functions, templates, type classes), JIT, parametric polymorphism. The type level programming is even more difficult for most developers to learn, and JIT will create jitter in the program(unlike JIT bytecode, here we must wait the code to be compiled to execute, unless we introduce one more bytecode layer). Only the last one is a balanced solution.

So my proposal is to enable parametric polymorphism there in the case only when interface generic method which is the same thing as system-F and its variants.

And it can clearly distinguish parametric polymorphism from adhoc polymorphism we have now, and provide type-safe way for developer to travel between the two kinds of polymorphism.

I have a real case in my hand which needs universal quantification over types.

// Scope need to remain polymorphic so I can replace it with different kinds of async scheduler
type Scope interface {
  Fork[T any](func() T) func() T
}

Currently I must use a type unsafe way to do it

// Scope implementation have to ensure the output type is same as input
// So we are back to the stone age before generics
type Scope interface {
  Fork(func() any) func() any
}

func Fork(scope Scope, f func() T) func() T {
  getter := scope.Fork(func() any) func() any {
    return f()
  }
  return func() T {
    getter().(T)
  }
}

And parametric polymorphism can save me from type casting and manually type checking, and it only do that thing, nothing else is applied to the program.

@Merovius
Copy link
Contributor

Merovius commented Jan 11, 2024

My proposal is not about the special cases discussed there. When I talk about "a special solution for cases which needs parametric polymorphism", the special relates to parametric polymorphism.

That is what I meant as well. It is used for a subset of cases. Making it strictly worse.

As I see the problem could only be solved in three ways […]

I tend to agree that there is a limited set of options to implement this feature. And that all the known ones so far have been excluded one way or another. Making it unlikely this feature is going to be implemented any time soon.

The way I personally deal with that knowledge is to act under the assumption that it will never get added. And if, at some point, someone comes up with a brilliant new solution or we dramatically change the design goals, I'll just be positively surprised.

And FWIW, I do think you are misusing some terms. Go does have parametric polymorphism (type parameters for functions and types). It just does not have rank 2 polymorphism (you can't pass around uninstantiated generic functions/types). And it does not have ad-hoc polymorphism (function overloading), but it does have subtype polymorphism (structural subtyping via interfaces).

@alice-sawatzky
Copy link

alice-sawatzky commented Feb 16, 2024

so, a subset of the desired behavior is to simply defer fully instantiating the type until you're ready to call the type's method:

// No instantiation on declaration of A
type A struct{}
type B struct{}
func (a A) Method[T any]() {}
a := A{}
// instantiation only happens here
a.Method[B]()

One thing worth considering: you don't have to instantiate a type only once, because you can create an alias of that type with the needed type parameter, which means in the current syntax you still can defer instantiation until the time of method call:

type A struct{}
type AWrapped[T any]
type B struct{}
func (a AWrapped[T]) Method() {}
a := A{}
AWrapped[B](a).Method()

@ianlancetaylor
Copy link
Contributor

@alice-sawatzky Given that Go is a compiled language, that appears to require runtime code generation, which we don't want to do.

@mjholub
Copy link

mjholub commented Feb 19, 2024

This will add so much complexity

And how having to either pass necessary parameters like logger (if not using a singleton or stdlib logger), database connection etc or a pointer to the method's receiver which contains these to generic functions when using dependency injection help consistency and simplicity?

I.e. instead of being able to write something like

package otherpackage

type SomeService struct {
db *pgxpool.Pool
log *zerolog.Logger
}

func (s *SomeService) Foo[T  Bar](ctx context.Context) (T, error){
// ...
}

One needs to write a generic function like this:

func Foo[T Bar](ctx context.Context, s *SomeService) (T, error)

Then let's say we have another module of our application, where
an instance of SomeService is embedded in another struct as baz.

Let s be the name by which we'll reference the struct embedding an instance of SomeService as baz.

Let's say SomeService also has an exported non-generic method Bar.
We can reference it like this: s.baz.Bar(params)

But a function using type constraints (we'll use the one from previous example) currently has to be referenced as otherpackage.Foo(ctx, s.baz), instead of just s.baz.Foo(ctx). This is counterintuitive and breeds inconsistency.

@nyanpassu
Copy link

nyanpassu commented Feb 22, 2024

My proposal is not about the special cases discussed there. When I talk about "a special solution for cases which needs parametric polymorphism", the special relates to parametric polymorphism.

That is what I meant as well. It is used for a subset of cases. Making it strictly worse.

As I see the problem could only be solved in three ways […]

I tend to agree that there is a limited set of options to implement this feature. And that all the known ones so far have been excluded one way or another. Making it unlikely this feature is going to be implemented any time soon.

The way I personally deal with that knowledge is to act under the assumption that it will never get added. And if, at some point, someone comes up with a brilliant new solution or we dramatically change the design goals, I'll just be positively surprised.

And FWIW, I do think you are misusing some terms. Go does have parametric polymorphism (type parameters for functions and types). It just does not have rank 2 polymorphism (you can't pass around uninstantiated generic functions/types). And it does not have ad-hoc polymorphism (function overloading), but it does have subtype polymorphism (structural subtyping via interfaces).

ad-hoc polymorphism doesn't only means function overloading, it means the behaviour of code relates to type. for example

type Foo interface {
  Foo()
}

func Foobar[T Foo]() {
  var t T
  t.Foo()
}

func main() {
  Foobar[X]() // X implements Foo
  Foobar[Y]() // Y implements Foo
}

in the above code, var t T and t.Foo() all depents on the actual type of T. But parametric polymorphism, it will not depends on the type of T(where t.Foo() will converts to subtyping polymorphism, but you need to provide a instance as parameter, that is func[T any](t T)). This kind of program exists in the form of parametric polymorphism but it has adhoc aspects, so actually they are adhoc polymorphism. You might argue that adhoc aspects should refer to the kinds like template specilization, but of course Golang has another typical adhoc polymorphism:

func Foobar(x any) {
    switch x.type:
    case X:
      x.Foo()
    case Y:
      x.Bar()
}

I'm not here to argue on this subject, but I'm here for a new proposal, and I apologize for I didn't go thought the whole thread to see if someone has the same proposal before.

The idea is just make program can't apply type argument to a interface type, but only can apply to a actual type with interface constraint. For example

type Foobar interface {
  Bar()
  Foo[T any]()
}

func Foobar0(t Foobar) {
    t.Bar() // passed, not generic method
}

func Foobar1(t Foobar) {
    t.Bar()
    t.Foo[int]() // rejected by compiler, generic method can't be applied to runtime objects
}

func Foobar2(t any) {
    t.(Foobar).Foo[int]() // rejected by compiler, generic method can't be applied to runtime objects
}

func Foobar3[T Foobar](t T) {
    t.Foo[int]() // passed, we can specialize T::Foo[int] at compile time
}

type Bar struct{} 

type (Bar) Foo[T any]() { fmt.Println("Bar") } 

func main() {
    Foobar3(Bar{})
}

I think the whole program doesn't need much more changes for the current go compiler, and it doesn't need any changes in the runtime representation at all. And it has forward compatibility if we can find other ways to support Foobar0, Foobar1 and Foobar2.

@Merovius
Copy link
Contributor

Merovius commented Feb 22, 2024

@nyanpassu ad-hoc, parametric and subtype polymorphism have established definitions that are incompatible with yours. It is confusing that you are using jargon in a way that is incompatible with established definitions.

I apologize for I didn't go thought the whole thread to see if someone has the same proposal before.

There is always time to rectify that.

The idea is just make program can't apply type argument to a interface type, but only can apply to a actual type with interface constraint.

I agree that what you suggest could be implemented within the constraints set forth for the generics proposal. But it has the same problem other, similar suggestions have: It essentially implies that generic methods do not participate in interface-satisfaction.

That is, from what I understand, under your rules, this program would panic:

type S struct{}
func (S) M[T any](T) {}

type J interface {
    M(int)
}

func main() {
    var x any = S{}
    x.(J) // panics
}

This is one of the issues brought up in the design doc to disqualify similar designs.

And I'll note that if you only allow statically dispatched generic method calls, you can always use a top-level function as well. That is, if you know the type statically, the package it's defined in can also provide a package-scoped function for you to call. From the design doc:

Or, we could decide that parameterized methods do not, in fact, implement interfaces, but then it's much less clear why we need methods at all. If we disregard interfaces, any parameterized method can be implemented as a parameterized function.

@nyanpassu
Copy link

nyanpassu commented Feb 22, 2024

@nyanpassu ad-hoc, parametric and subtype polymorphism have established definitions that are incompatible with yours. It is confusing that you are using jargon in a way that is incompatible with established definitions.

I apologize for I didn't go thought the whole thread to see if someone has the same proposal before.

There is always time to rectify that.

The idea is just make program can't apply type argument to a interface type, but only can apply to a actual type with interface constraint.

I agree that what you suggest could be implemented within the constraints set forth for the generics proposal. But it has the same problem other, similar suggestions have: It essentially implies that generic methods do not participate in interface-satisfaction.

That is, from what I understand, under your rules, this program would panic:

type S struct{}
func (S) M[T any](T) {}

type J interface {
    M(int)
}

func main() {
    var x any = S{}
    x.(J) // panics
}

This is one of the issues brought up in the design doc to disqualify similar designs.

And I'll note that if you only allow statically dispatched generic method calls, you can always use a top-level function as well. That is, if you know the type statically, the package it's defined in can also provide a package-scoped function for you to call. From the design doc:

Or, we could decide that parameterized methods do not, in fact, implement interfaces, but then it's much less clear why we need methods at all. If we disregard interfaces, any parameterized method can be implemented as a parameterized function.

Emmm, firstly, type class is a way to support adhoc polymorphism in type system, and using type bound for statical dispatch is not far away from that. And if you check the definition of adhoc polymorphism, you will find it also refers to such kind of codes which also exists in Golang

void foobar(x Object) {
  if (x instanceof Integer) {
     ...
  }
  if (x instanceof Number) {
     ...
  }
}

Yes I mean the program will panic. (I have edit this post, the previous one is wrong. sorry I'm a little confused by myself somehow, maybe because it's late night in my timezone).

type S struct{}
func (S) M[T any](T) {}

type J interface {
    M(int)
}

func main() {
    var x any = S{}
    x.(J) // panics
}

I think it's acceptable. And actually S::M has type forall t. t -> unit and J::M has type int -> unit, they are not the same type, so it's totally expected.

And one more thing is that we will not be able to use interface object in such condition:

type Foobar interface {
  Foo[T any]()
  Bar()
}

type X struct {}
func (X) Foo[T any]() {...}

func Foobar0[T Foobar](t T) {
    t.Foo[int]() // passed, we can specialize T::Foo[int] at compile time
}

func main() {
   var foobar Foobar = X{} // interface objects doesn't have generic method
   Foobar0(foobar) // rejected
    // Foobar{ Bar() } doesn't satify the constrint of 
    // Foobar{   
    //   Foo[T any]()
    //   Bar() 
    // }
}

This might be strange for some users but it's acceptable. And actually it's the same way that Rust used to imple dyn objects(which is similar to Go interface), but they didn't allow any dyn objects from generation if the definition contains type specific behaviour. However it's practical that we generate runtime object which only has part of the definition of the interface.

We can't always use a top-level function, because I need polymorphism in this case:

type Scope interface {
   Fork[T any](func() T) func() T
   Join()
}

func Run[S Scope](scope S) {
    f1 := s.Fork(func() int { ... })
    f2 := s.Fork(func() string { ... })
    s.Join()
    return f1() + len(f2())
}

I need the implementation of scope can be changed. If I can only use top-level function, I can't write program simply in this way. Without rank-n types, it's really hard to implement generic reactive library in Golang.

@Merovius
Copy link
Contributor

I think it's acceptable.

That is fine. Many people in this thread have come up with designs they personally find acceptable. But that is not the decision criterion for inclusion into Go. As long as the people who make that decision do not find it acceptable, it is probably most helpful to try and understand why.

@nyanpassu
Copy link

nyanpassu commented Feb 23, 2024

I think it's acceptable.

That is fine. Many people in this thread have come up with designs they personally find acceptable. But that is not the decision criterion for inclusion into Go. As long as the people who make that decision do not find it acceptable, it is probably most helpful to try and understand why.

I have explain my reason for that panic is acceptable is because a type

interface {
  M[T any](T)
}

is not the same as the type

interface {
  M(int)
}

Because the previous one has quantification on the method.
So it's the same question as is type forall a. a -> unit the subtype of int -> unit.
Well we can make it stand but only if we using dynamic dispatch for generics.
As golang have chosen to use static dispatch for generics, the decision to deny the above subtyping relationship is reasonable.

It's just like the same question that people will ask why no covariance/contravariance in Golang

func Foo(func(interface{ 
  Foo() 
  Bar() 
})) {}

func Foobar() {
  Foo(func(x interface{ Foo() }) {}) // compile error
}

@Merovius
Copy link
Contributor

I have explain my reason for that panic is acceptable

Yes. Don't worry, I understand that you find your solution acceptable and I understand your reasoning for that.

Did you understand mine? About it being undesirable to have generic methods, if they can't be used in interfaces?

As golang have chosen to use static dispatch for generics

To be clear: The decision was to not require either.

It's just like the same question that people will ask why no covariance/contravariance in Golang

Yes. I would prefer Go to have Co- and Contravariance for functions and methods. But it's not up to me, the Go team disagrees and I can accept that.

@nyanpassu
Copy link

nyanpassu commented Feb 25, 2024

I have explain my reason for that panic is acceptable

Yes. Don't worry, I understand that you find your solution acceptable and I understand your reasoning for that.

Did you understand mine? About it being undesirable to have generic methods, if they can't be used in interfaces?

As golang have chosen to use static dispatch for generics

To be clear: The decision was to not require either.

It's just like the same question that people will ask why no covariance/contravariance in Golang

Yes. I would prefer Go to have Co- and Contravariance for functions and methods. But it's not up to me, the Go team disagrees and I can accept that.

Sorry, about being undesirable to have generic methods, if they can't be used in interfaces?, I'm not sure what does it means. Because interface in Golang can be used as a type constraint, or type of runtime object. To me it's not clear which case you mean or all of them.

Currently we can't use interface contains type set as a runtime object but only as type constraint, so I guess what you mean is not that interface with generic method must can be used as runtime objects, because currently we can't use every interface as runtime objects.

type TypeSet interface { T1 | T2 }
func Foobar(t TypeSet) { ... } // error, can only use TypeSet as type constraint

Because type constraint only lives at compile time, so we don't need to worry about that we can't use generic method in type constraint, just make interface with generic method can't be used as a type.

But indeed Golang has a plan to support using type set as union type at runtime. So maybe you indeed mean that if Golang is going to support generic method, then interface with generic method must can be used as a runtime objects.

For a function of the type like forall T. X (X is a type expr which contains type variable T), programming languages have two different approach:

  1. it only equals to a proof for the program is correct, the function has a unique representation for every type
  2. not only it equals to a proof but also it's a calculation at compile time(statical dispatch) to get the function for the type, the function has different representation for different type(so that's why I treat it as polymorphism with adhoc aspect, currently we can use it to simulate function overloading)

Let discuss them seperately. For the first one, we must have unique implementation for every type at runtime. Using this approach means type erasure which many people had discussed about it and were denied. My very first suggestion is only add a keyword to mark it explicitly so coders knows the abstraction behind it(I don't know whether it's the cost of abstraction making it denied or the abstraction itself).

For the second one, either we must collect every potential type application at compile time, including the following one:

type HasIdentity interface { Identity[T any](T) T }
type HasIntIdentity interface { Identity(int) int }

We must treat HasIntIdentity::Identity a type application of HasIdentity::Identity[int], which will add too much complexity to the compiler. Or we can introduce JIT compiler, which I think was also discussed in this thread is not worthy to do.

So my final thought is that, we can use interface with Generics as type constraint only just like type set now. And if there is further option to use it as runtime objects, at that time we can try to use it as runtime objects just like the option to use type set as sum type. It's fully forward compatible.

type HasIdentity interface { Identity[T any](T) T }
func Foobar(a any) {
   a.(HasIdentity).Identity(1) // rejected by compiler, HasIdentity can only be used as type constraint
}

I've gone throught the whole thread roughly and I didn't find similar suggestions, and I apologize if there are any(as it's so simple someone must have the same idea before).

@AugustDev

This comment was marked as duplicate.

@gucio321
Copy link

gucio321 commented Mar 25, 2024

Hi everyone. Let me add something from my self too.
I'm following this topic since about a year. Every time see a notification from here, the following comes to my mind: "The hell, why they didn't fix that yet? Is that really so difficult?"

This topic however produced so many posts that it is really impossible to read them all to know the whole context so forgive if I miss something.

I'd like to rfer the very beginning of this conversation where it is said that:

The document also explains what the problems are. So what are your solutions to these?

Let me mentioned code here
package p1

// S is a type with a parameterized method Identity.
type S struct{}

// Identity is a simple identity method that works for any type.
func (S) Identity[T any](v T) T { return v }

package p2

// HasIdentity is an interface that matches any type with a
// parameterized Identity method.
type HasIdentity interface {
	Identity[T any](T) T
}

package p3

import "p2"

// CheckIdentity checks the Identity method if it exists.
// Note that although this function calls a parameterized method,
// this function is not itself parameterized.
func CheckIdentity(v interface{}) {
	if vi, ok := v.(p2.HasIdentity); ok {
		if got := vi.Identity[int](0); got != 0 {
			panic(got)
		}
	}
}

package p4

import (
	"p1"
	"p3"
)

// CheckSIdentity passes an S value to CheckIdentity.
func CheckSIdentity() {
	p3.CheckIdentity(p1.S{})
}

I think the argument here is irrelevent

Now I'm reading the justification below the code:

Therefore, the function p3.CheckIdentity can call vi.Identity with an int argument, which in the call from p4.CheckSIdentity will be a call to p1.S.Identity[int]. But package p3 does not know anything about the type p1.S. There may be no other call to p1.S.Identity elsewhere in the program. We need to instantiate p1.S.Identity[int] somewhere, but how?

and it doesn't make sense for me at all! the fact that "p3 does not know anything about the tpe p1.S" is what the interface is...
If you want some package doesn't know anything about other package (also: type) you use interface. Why this statement is irrelevent is that WE DON'T NEED TYPE METHODS to achive that. You can do exactly the same effect with just a normal interface and type parameter has nothing to do here.

**Edit1** here is an example
package main

import "fmt"

// package p1

type S struct{}

func (s *S) N(v int) int {
	return v
}

// package p2

type U interface {
	N(int) int
}

// package p3

func UseU(v interface{}) {
	if vi, ok := v.(U); ok {
		fmt.Println(vi.N(42))
	}
}

// package p4

func main() {
	UseU(&S{})
}

Alternative solution

If you really don't know how to implement that interface stuff , I'd suggest to allow such a methods but diallow casting them into an interface.

Let me know if I missed something.

@Merovius
Copy link
Contributor

@gucio321

You can do exactly the same effect with just a normal interface and type parameter has nothing to do here.

The crux of the problem is that with a parametric method, the generated code for the body depends on the type-parameter (e.g. addition on integers requires different CPU instructions than addition of floating point numbers). So for a generic method call to succeed, you either need to know all combinations of type-arguments when compiling the body (so that you can generate all bodies in advance), or you need to know all possible concrete types that a dynamic call could get dispatched to, when compiling the call (so you can generate the bodies on demand). The example from the design doc breaks that: The method call is via an interface, so it can dispatch to many concrete types and the concrete type is passed as an interface, so the compiler doesn't know the call sites.

With your example, this is not a problem, because the method body involves only a single concrete type. So when the compiler encounters the method, it just generates code for that and any method call can only ever refer to that one body.

If you really don't know how to implement that interface stuff , I'd suggest to allow such a methods but diallow casting them into an interface.

That has been discussed and it seems that if we can't use generic methods to implement interfaces, that the cost of adding them (which then includes the confusion that some methods can't implement interfaces) is not worth the benefit.

You might well disagree with that statement and feel that some of the alternatives are worth it. But Go is, at the end of the day, governed by the Go team, which decide these matters based on their own experience and preference. Personally, I like it that way, because I think that having a somewhat consistent vision is good for a project such as this. You might disagree with that as well, in which case the most fundamental software-freedom is the freedom to fork.

@wxblue
Copy link

wxblue commented Mar 26, 2024

func Map[TIn any, TOut any](in *Stream[TIn], f func(TIn) TOut) *Stream[TOut] 
var in *Stream[TIn]
out := Map(in, func(in TIn) TOut { return TOut{} })

Generic function can realise Map function, but it is not easy to achieve chain invoke, so we just need a little syntax sugar to make function invoke like method invoke, don't need to care about interface and so on, just handle it at compile time, like:

out := in:Map(func(in TIn) TOut { return TOut{} })

@ireina7
Copy link

ireina7 commented Mar 26, 2024

func Map[TIn any, TOut any](in *Stream[TIn], f func(TIn) TOut) *Stream[TOut] 
var in *Stream[TIn]
out := Map(in, func(in TIn) TOut { return TOut{} })

Generic function can realise Map function, but it is not easy to achieve chain invoke, so we just need a little syntax sugar to make function invoke like method invoke, don't need to care about interface and so on, just handle it at compile time, like:

out := in:Map(func(in TIn) TOut { return TOut{} })

generic methods are NOT just about chain invoke. They are also about how you abstract your program(especially with interface) and reduce software complexity by generic constraints.

@atdiar
Copy link

atdiar commented Mar 26, 2024

Interesting to note that this problem is linked to one of the very first decisions in Go, namely whether len, cap et.al. should be functions or methods. :)

@andig
Copy link
Contributor

andig commented Mar 29, 2024

Fwiw, here's one thing I'd like to do if I had type parameters, note the second method which does not depend on any type parameter of Settings. Maybe it would be possible to enable a subset of use cases?

func (s *Settings) String(key string) (string, error) {
	if s == nil {
		return "", nil
	}
	return settings.String(s.Key + key)
}

func (s *Settings) StringAs[T encoding.TextUnmarshaler](key string) (T, error) {
	var t T
	s, err := settings.String(key)
	if err == nil {
            err = t.UnmarshalText([]byte(s))
	}
	return t, err
}

@Merovius
Copy link
Contributor

@andig Why not

func (s *Settings) UnmarshalInto(key string, p encoding.TextUnmarshaler) error {
    v, err := s.String(key)
    if err != nil {
        return err
    }
    return p.UnmarshalText([]byte(v))
}

It's a little bit more to type at the call-site, but I don't see why you need generics at all here.

off-topic, but I'll also note that your example probably wouldn't work, you'd need

type TextUnmarshalPointer[T any] interface {
    *T
    encoding.TextUnmarshaler
}

func (s *Settings) StringAs[T any, PT TextUnmarshalPointer[T]](key string) (T, error) {
	var t T
	s, err := settings.String(key)
	if err == nil {
            err = PT(&t).UnmarshalText([]byte(s))
	}
	return t, err
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
generics Issue is related to generics LanguageChange Proposal Proposal-Hold v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests