Here is my whack at a proposal for how Generics might look in Go.
I think generics should be mechanism for re-using tested, optimized code and for increasing readability of source, not for saving developers a few keystrokes, or fulfilling their desire to write things that look like equations.
Therefore I do not propose overloading operators (or anything much actually), indeed I actively oppose allowing it.
Also, I think compile-time polymorphism (use any type that works, as long as you know what it is at compile time) is more important than run-time polymorphism (use multiple different types in the same structure and iterate over them letting the run-time call the right method for each type).
For example, a generic Keys function signature might look like this (in this case specifiying the package generic
):
func Values< K generic.Keyer, V generic.Slicer>(m map[K]V) []V {
The interface types contain the behaviours the types given as parameters must support. The two given here (Keyer and Slicer) and other, very common, behaviours are defined in a standard library package generics
. I anticipate that builtin
would declare methods to implement these for most pre-declared basic types.
Personally, fwiw :), I prefer angled to regular brackets around the type params.
For example, somewhere close to the call site one might say:
import ( mc mycrazystuff )
...later...
use ( min mc.Min< mc.Craaazy> ) // call must be in regular execution scope from here. Perhaps should only allow things with <>.
This binds the type parameter to an explicit type, if the explicit is an interface type the compiler would know to treat built-in types as objects, with associated performance penalties; otherwise it does not have to (much hand-waving here). This allows a compact form for calling the generic func (min
), while ensuring that this contraction is specified in the same scope.
Note, however, the required repetition of the home package of the generic, providing a cue that behaviour may not be exactly as you might expect from something called min
. Should uppercase be required on the contraction is this case? (Min
) ... I cannot decide.
var a mc.Craaazy = 1729
var b mc.Craaazy = 42
c := mc.min(a,b)
Having the use declaration allows us to dispense with most (all?) type inference I believe.
For example:
type Lesser interface {
Less(b same) bool
}
Here, same
evaluates to the type of the receiver when the method is actually defined.
Within same
lie the devilish details for the compiler, I suspect. Typically, the interface definition and the method definition will be in different packages -- currently forbidden -- and the compiler will have to drag around the definitions. All I can say is that I think the above minimizes this problem.
This method definition does similar work to a 'contract' in the proposal.
For example, in a package like builtin
:
func (a int) Sliceable {
can { make([]same) }
}
Here, can
makes this a no-op, doing the same job as _ :=
would do in _ := can make([]same)
but without making the compiler or linters angry -- it's just insisting that we can compile the code within. Source code within a can
block produces no executable code.
----------- generic.go (system std package)
package generic
...
type Lesser interface {
Less(b same) bool
}
type Keyer interface {
Keyable()
Sliceable()
}
type Mapper interface {
Mappable()
Sliceable()
}
type Slicer interface {
Sliceable()
}
type Donner interface { ...
type Blitzen interface { ...
func (a int) Less(b int) (bool) {
if a<b {
return true
}
return false
}
func (a int) Keyable {
can { make(map[same]int) }
}
func (a int) Mappable {
can { make(map[int]same) }
}
func (a int) Sliceable {
can { make([]same) }
}
----------- mypack.go
package mypack
import (
gen generic
)
func Min<T gen.Lesser>(a, b T) (T) {
if a.Less(b) {
return a
}
return b
}
func MinS<T gen.Lesser>(a []T) (T) {
...
m := a[0]
for _, v := range a {
if v.Less(m) {
m = v
...
func Keys<K gen.Keyer, V gen.Slicer>(m map[K]V) []K {
...
ks := make([]K)
for k := range m {
ks = append(ks, k)
...
func Values<K gen.Keyer, V gen.Slicer>(m map[K]V) []V {
...
vs := make([]V)
for v, _ := range m {
...
----------- main.go
package main
import pack
func main() {
use (
MinS mypack.MinS<int> // It's up to the programmer, not the compiler, to disambiguate on type
Min mypack.Min<int> // Allowing the type parameter to be an interface type would allow iterating over run-time polymorphic
) // structures but may lead down various rabbitholes -- is it worth it?
var a int = 1729
var b int = 42
c := mypack.Min(a,b) // using "use" definition -- slightly more compact
d := mypack.Min<int>(a,b) // not using "use" definition
f := []int{114, 35, 1729, 42}
g := mypack.MinS(f)
H := mypack.MinS<int>(f)
...
This is, of course, very simplistic -- 'FRI' means 'Fools Rush In ...' -- addressing something on which minds immeasurably superior to my own have gnawed for years. I elide over implementation difficuties, because I don't know what they might be, let alone understand how to resolve them.
I also confess I have not read all the other proposals -- after the first half dozen or so I felt in danger of spraining my braincell -- so it is entirely possible that this is functionally identical to someone else's proposal.
If one sees c := a + b
in a program, that should mean the same thing whether or not a and b are simply ints
or some crazy structure defined in an imported library and for which +
actually has a subtly different meaning than for scalar numbers or strings. If, however, one explictly marks a word-named function, Less
say, in some current scope as being derived from a generic package then I believe that is sufficient notice to the reader that underlying complexity may be afoot.
I believe that Rob Pike's observation that 'Simplicity is complicated' should not lead us into thinking that adding underlying complexity with superficial simplification neccessarily produces net simplification. I think overloading for compile-time polymorphism (a type of superficial simplicity) is a net loss of simplicity since it requires the compiler to make ever more guesses about the programmer's intent. I love compiler errors! Overloading should be restricted to the only case it is essential (I think): run-time polymorphism.
I think the above elements: use
, can
, same
, <>
add fairly minimal syntax to the language and function, and tell both readers and the compiler exactly what is wanted.