Skip to content

Instantly share code, notes, and snippets.

@aprice2704
Last active December 5, 2018 00:25
Show Gist options
  • Save aprice2704/c04c8f8efd5d847e43007dc7c8904629 to your computer and use it in GitHub Desktop.
Save aprice2704/c04c8f8efd5d847e43007dc7c8904629 to your computer and use it in GitHub Desktop.
FRI Generics for Go (golang) Proposal

FRI Go Generics Proposal

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).

Elements of the proposed syntax

1. Type parameter clause specifying an interface type at the implementation site

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.

2. use declaration binding an explicit type and a function name in the scope of the call site

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.

3. Function calls become rather plain at the call site

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.

4. New keyword same in interface declarations

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.

5. New keyword can in method definition

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.

Whole Example

A system package defining a pile of interfaces that each basic/built-in type can satisfy:

----------- 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) }
}

A package defining some generic functions:

----------- 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 {
 ...

A program using the generics:

----------- 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)
  ...

Disclaimers & Blather

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.

Main Design Consideration

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.

Conclusion

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment