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: generics: type initialization with value parameter #67064

Open
3 of 4 tasks
smyrman opened this issue Apr 26, 2024 · 0 comments
Open
3 of 4 tasks

proposal: generics: type initialization with value parameter #67064

smyrman opened this issue Apr 26, 2024 · 0 comments
Labels
LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@smyrman
Copy link

smyrman commented Apr 26, 2024

Go Programming Experience

Experienced

Other Languages Experience

C, Python

Related Idea

  • Has this idea, or one like it, been proposed before?
  • Does this affect error handling?
  • Is this about generics?
  • Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit

Has this idea, or one like it, been proposed before?

Yes

Typed enum support has been suggested in #19814. That proposal has a overlapping use-case, which is to allow conversion to/from enumerated values and strings.

This proposal is different in that it's not limited to enums; it's about type parameterization using values. This proposal could be used to define an enum package (in or outside) the standard library to aid with enum conversion and validation, but it's not primarily a proposal for implementing enums.

Const parameters has been suggested in #65555. That proposal aims at allowing to create types using constant parameters (as an alternative or addition to typed parameters).

This proposal is different, and perhaps arguably worse, in that it isn't limited to constant parameters. The proposal is also different in the sense that it's aiming for a clear separation between type and value parameters.

Does this affect error handling?

No

Is this about generics?

Yes, it's about allowing type initialization to refer to a value.

Proposal

Sometimes, it would be useful to initialize a type that is initialized not only using type parameters, but also value parameters. This allow generic code to be written for multiple use-cases, such as:

  • Initialize generic types that reference static values. E.g. a model type could include a function TableName() string that return a static result.
  • Initialize generic enum-like types with conversion to and from an external representation by providing a two-way lookup value reference (see example) by implementing interfaces that are relevant to the target context. E.g. in the context of database fields, define a generic type with sql.Scaner and driver.Valuer implementations refering to a type parameterized translation lookup. In the context of an API model, implement encoding.TextMarshaller and encoding.TextUnmarshaller instead.
  • Initialize arrays or matrices of a specific size (maybe more complex).

Enum use-case

Goal: define an enum like value with minimal boiler plate (library code is allowed).

Current solution (no language change)

Library:

package enum

// Lookup provides a two-way lookup
// between values of type T and strings.
type Lookup[T comparable] interface{
   Value(string) (value T, ok bool)
   Name(T) (name string, ok bool)
}

func MustCreateLookup[T comparable](map[T]string) EnumLookup{
    // implementation doesn't matter.
})

Application:

type MyType uint
 
const (
    _ MyType = iota
    MyTypeValue1
    MyTypeValue2
)

var myTypeLookup = NewEnumLookup[MyType](map[MyType]string{
   MyTypeValue1: "value1",
   MyTypeValue2: "value2",
})

func (v MyType) Value() (driver.Value, error) {
    v, ok := myTypeLookup.Name(v)
    if !ok {
        return fmt.Errrof("unrecognized value: %v", v)
    }
    return v, nil
}

func (v *MyType) Scan(src any) error {
     s, ok := src.(string)
     if !ok {
         return fmt.Errorf("incompatible type: %T", src)
     }
     _v, ok := myTypeLookup.Value(s)
     if !ok {
         return fmt.Errrof("unrecognized value: %v", s)
     }
     *v = _v
}

Suggested solution (language change)

Library:

package enum

// Lookup provides a two-way lookup
// between values of type T and strings.
type Lookup[T comparable] interface{
   Value(string) (value T, ok bool)
   Name(T) (name string, ok bool)
}

func MustCreateLookup[T comparable](map[T]string) EnumLookup{
    // implementation doesn't matter.
})
package enumsql

type Enum[L enum.Lookup[T], T comparable][lookup L] T

func (v Enum[L,T][lookup]) Value() (driver.Value, error) {
    v, ok := lookup.Name(v)
    if !ok {
        return fmt.Errrof("unrecognized value: %v", v)
    }
        return v, nil
}

func (v *MyType) Scan(src any) error {
     s, ok := src.(string)
     if !ok {
         return fmt.Errorf("incompatible type: %T", src)
     }
     v, ok := myTypeLookup.Type(s)
     if !ok {
         return fmt.Errrof("unrecognized value: %v", s)
     }
     *v = _v
}

Application:

type MyType = sqlenum.Enum[uint](enum.MustCreateLookup[MyType](map[MyType]string{
   MyTypeValue1: "value1",
   MyTypeValue2: "value2",
})
 
const (
    _ MyType = iota
    MyTypeValue1
    MyTypeValue2
)

Matrix use-case

An attempt of supporting what's described in #65555. There could be reasons in which this is harder than the first case. I am not sure it's a priority use-case.

Current solution (not type safe)

type Matrix[T any] struct{
    data []T
    w, h int
}

func (m Matrix[T]) Index(x, y int) (v T, bool) {
    if 0 < x || x >= w {
        return v, false
    }
    if 0 < y || y >= h {
        return v, false
   }
    return m[x+y*w], true
}

func NewMatrix[T any](w, h int) Matrix {
    if w <= 0 || h <= 0 { panic("invalid size")}
    return Matrix{data: make(w*h), w: w, h: h}
}

Solution with language change (type safe)

func NewMatrix[T any](n, m int) [n][m]T {
    return [n][m]T{}
}

Alternatively, if it's easier for implementation reasons:

func NewMatrix[T any][n, m int]() [n][m]T {
    return [n][m]T{}
}

Language Spec Changes

To be decided; early draft

Type value parameter declarations

A type value parameter list declares the type value parameters of a generic type declaration; it can not be used by generic functions. The type value parameter list looks like an ordinary function parameter list except that the type parameter names must all be present and the list is enclosed in square brackets rather than parentheses. The declaration must come right after a type parameter list. If there are no type parameters, then an empty set of square brackets must precede the value parameter list.

Example (with a type parameter list prefix):
[T any][v T]
[][v string]
[][_ any]

Just as each ordinary function parameter has a parameter type, each type value parameter has a type.

Informal Change

Just like a function can have both input parameters and output parameters, a generic type can have both type and type value parameters. Both parameters are used to generate a concrete implementation. Type parameters are replaced by specific types, while type value parameters are replaces by a reference to a specific variable reference. That is, if you where to generate code for it, you would typically see that instances of the type parameters are replaced by the proper type reference, while references to values, are replaced by references to package scoped variables.

A simple (and somewhat silly) example:

type Greeter[][v string] string

func (g Greeter[][v]) Greet() string {
   return fmt.Sprintf("%s %s!", v, g)
}

type Helloer = Greeter[]["Hello"]

// Hello Peter!
func main() {
  peter := Helloer("Peter")
  fmt.Println(peter.Greet())
}

Is this change backward compatible?

Yes

Orthogonality: How does this change interact or overlap with existing features?

It interacts with generic type declarations, by declaring a second parameter group.

Would this change make Go easier or harder to learn, and why?

I don't think it would make Go easier to learn. It's going to make it somewhat harder to learn generics in particular.

It might help deal with some pain-points if combined with the right standard libraries, such as easier enum handling.

Cost Description

  • Generics just got twice as complex.
  • You know have types declarations that need to point to variable declarations. The compiler guys will not like this; they may decide to quit Go altogether.
  • Should consider possible misuse. What's the worst code you can write with this feature?

Changes to Go ToolChain

Yes

Performance Costs

Unknown.

Prototype

I belie a ad-hock implementation could be written that depend on code generation.

Something similar to what was used for go generics, using .go2 or another file type format, could be used to scan the code and replace type value parameters with specific type implementations that replace the variable reference with a reference to a private package scoped variable; potentially one with a generated name.

I.e.

type Greeter[][v string] string

func (g Greeter[][v]) Greet() string {
   return fmt.Sprintf("%s %s!", v, g)
}

type Helloer = Greeter[]["Hello"]

would generatesomething like:

var _go2generated_Helloer_p1 string = "Hello"

type Helloer string

func (g Helloer) Greet() string {
   return fmt.Sprintf("%s %s!", _go2generated_Helloer_p1, g)
}

The proto-type could be limited not to allow usage of the new generics across package borders, and could run before compilation. It could also require explicit use of type aliases for declaring types.

A final implementations should not have such limitaitons.

@smyrman smyrman added LanguageChange Proposal v2 A language change or incompatible library change labels Apr 26, 2024
@gopherbot gopherbot added this to the Proposal milestone Apr 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

2 participants