Skip to content

proposal: Go 2: Type parameterized interface default methods #33818

Closed as not planned
@smyrman

Description

@smyrman

This proposal is motivated by the following article. The proposal have some similar characteristics to #33410, but takes a different approach, and aims for the built-in types to implement interfaces rather than contracts. The proposal could hopefully take some inspiration implementation wise from the initial contracts proposal, where types where considered to implement a contract if a certain stub of code compiled.

The motivation for this proposal is to allow code based on interfaces to cover some (additional) generic use cases. The goal is not to create an extensive generics proposal, but rather to improve Go in a limited way that adds real value, and could make the language more fun and productive.

There is a lot you cannot do with this proposals, that you can do with other, more extensive, proposals; most noteworthy the latest contracts draft proposal. However, a goal would be for a final version of this proposal to be designed in such a way that it provides well founded building-blocks for adding more type parameterization to the language through future language feature proposals.

Proposal overview

The proposal is to allow an interface to declare default method implementations that utilize type parameterization of the method receiver type. The proposal should be differentiated from previous proposals to add interface default methods without type parameterization, in that this proposal can add real value. It is also distinguishable from other generics proposals in that it can likely allow existing code based on interfaces to be changed in a backwards-compatible way to support more types.

With the proposed default method implementations, a significant portion of the burden of implementing an interface can be moved from the package user to the package maintainer in some well-suited cases. A type that lacks a given function declared in the interface could be considered to still implement the interface if the default method implementation compiles for the given type.

The following characteristics will apply for interface default method implementations:

  • An implementation's signature must match a signature declared in the interface method set. It is not possible to provide default implementations for any method signature that isn't part of the interface method set.
  • A declaration is distinguishable from methods defined on types by the fact that the method receiver type is parameterized. It is not possible, as part of this proposal, to parameterize any other parameter type than the method receiver type.

The following characteristics match the behavior expected for other Go types:

  • It is not possible to declare more than one default method with a given name or signature on a given interface.
  • Interface default methods are inherited trough embedded fields the same way type methods are inherited for embedded fields in structs. When two or more embedded interfaces declare default implementations for the same signature, none of the implementations are inherited to prevent ambiguousness.
  • Implementations of default methods on an interface override any implementation made available through embedded fields on the interface.

Example syntax

This is an example syntax only, used to illustrate the proposal.

The example syntax follows the following rules:

  • If a method receiver type name is prefixed by a dollar-sign $, the type is considered to be parameterized. The compiler will need to replace all occurrences of $<TYPE NAME>, with the type that the method is being compiled for.

For this proposal in particular, the syntax can be used in two places:

  1. For the receiver type of an interface default method declaration line.
  2. To access the receiver type within an interface default method body.
type Equaler interface {
    Equal(other interface{}) bool
}

func (e $Equaler) Equal(other interface{}) bool {
    o, ok := other.($Equaler)
    return ok && o == e
}

This is the minimal syntax addition I can imagine for making the receiver type on a method parameterized. A final syntax should be developed to ensure that it's reusable for adding more generics functionality through later proposals.

Example use-cases

sort

Consider that the sort.Interface type is updated with the following type parameterized default methods:

type Interface interface{
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}
func (s $Interface) Len() int { return len(s) }
func (s $Interface) Less(i, j int) bool { return s[i] < s[j] }
func (s $Interface) Swap(i, j int) {s[i], s[j] = s[j], s[i] }

With this three lines of additional code, all of the following types would become eligible for direct use with the sort.Sort and sort.Stable functions:

  • Slices of comparable types ([]<comparable type>)
  • Maps of int to comparable types (map[int]<comparable type>)
  • Fixed-size arrays of comparable types ([N]<comparable type>)

For cases where an adapter type is needed, e.g. []MyStruct, explicit method must only be added for the cases where the coo-responding default implementation doesn't compile:

// AscGivenName allows ordering users by GivenName in ascending order.
type AscGivenName []User

func (s AscGivenName) Less(i, j int) bool {
    return s[i].GivenName < s[j].GivenName
}

// AscSurname allows ordering users by Surname in ascending order.
type AscSurname []User

func (s AscSurname) Less(i, j int) bool {
    return s[i].Surname < s[j].Surname
}

Example of ordering by Surname, then GivenName through use of stable sort:

users := []User{ ... } // consider to contain several entries.
sort.Order(AscGivenName(users))
sort.Stable(AscSurname(users))

Room for future extension

An important goal for this proposal is to ensure that it can be added in such a way that it doesn't only fit orthogonally into existing Go language features, but in a way that facilitates future extension.

Perhaps everything described in the contracts draft proposal doesn't need to become possible, but there are some key concepts that I think should remain possible to implement.

E.g. I believe interface type parameterization must eventually be allowed more places. We can for instance imagine some form of the contracts draft proposal using interfaces in place of contracts:

type hasher interface {...} // built-in interface or contract

type SyncMap(K hasher, V interface{}) struct {
    l sync.RWMutex
    m map[K]V
}

func (sm *SyncMap(K hasher, V interface{})) Set(k K, v V) {
    sm.l.Lock()
    sm.m[k] = v
    sm.l.Unlock()
}

func (sm *SyncMap(K hasher, V interface{})) Get(k K) (V, bool) {
    sm.RLock()
    v, ok := sm.m[k]
    defer sm.RUnlock()
    return v, k
}

This is an indication that the example syntax used to illustrate the proposals isn't a good final syntax.

Further work

To proceed with this proposal, I need help. Here are some topics I can think of:

  • Are there any obvious issues with this proposals that makes it hard to implement?
  • Are there more use-case where this proposal will work well?
  • Can a proto-type of this reasonably be implemented?
  • Can a syntax be developed that works well for this proposal and allows future extension?

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeLanguageChangeSuggested changes to the Go languageProposalgenericsIssue is related to genericsv2An incompatible library change

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions