Skip to content

Instantly share code, notes, and snippets.

@pat42smith
Last active September 26, 2018 17:29
Show Gist options
  • Save pat42smith/ed63aca983d4ba14fdfa320296211f40 to your computer and use it in GitHub Desktop.
Save pat42smith/ed63aca983d4ba14fdfa320296211f40 to your computer and use it in GitHub Desktop.
Go Generics for built-in and user-defined type parameters

Go generics for built-in and user-defined type parameters

Contracts — Draft Design presents a draft proposal for adding generic types and functions to the Go programming language, using contracts to specify the behavior required of type parameters.

The current document sketches a simple extension to that proposal, which would permit writing generic functions whose type parameters could be either built-in types or user-defined types deliberately designed to be similar to the built-in types.

Background

Consider a generic function, say

func Sum(type T)(list []T) T	// Returns the sum of the elements of list

Suppose the intent is that the type parameter T can be either a type built in to the language, say any numeric type, or a user-defined type with similar semantics, such as an arbitrary precision integer type defined as a wrapper around *big.Int from the math/big package.

type BigInt *big.Int
func (a BigInt) Add(b BigInt) BigInt {
	return big.NewInt(0).Add(a, b)
}
// more methods

How can we write the body of Sum? One attempt might use type switches or type assertions, or some variation thereof.

type Adder(type T) interface {
	Add(T) T
}

func Sum(type T)(list []T) T {
	var sum T
	if a, ok := sum.(Adder); ok {
		for _, t := range list {
			sum = sum.Add(t)
		}
	} else {
		for _, t := range list {
			sum += t
		}
	}
	return sum
}

This has some difficulties. First, the version above requires that T be usable with the + operator, even if that branch will never be executed because T has an Add method. This might be avoidable using reflection, but the resulting Sum will not be typesafe; if called with an unusable type T, it will panic at run time instead of failing at compile time. Second, depending on how contracts work, it may be difficult or impossible to write a contract for this Sum.

Another way to write sum is to require the caller to pass an additional parameter encapsulating the behavior required of T. This is common with sorting functions, which often take a parameter describing how values are to be compared. We could write Sum as follows.

func Sum(type T)(list []T, add func(a, b T) T) T {
	var T sum
	for _, t := range list {
		sum = add(sum, t)
	}
	return sum
}

This also works, but requires a fair bit of extra code. Many calls to generic functions will require one or more additional parameters, and many small adaptor functions will have to be written. Adding irritation, much of this extra code will be simple boilerplate, entirely predictable from the types involved.

In this document, we propose adding a new package operators to the standard library. This would contain many small generic functions corresponding to the built-in operators of Go. For example, there might be an Add function, which is defined as

func Add(type T)(a, b T) T { return a.Add(b) }

when T has a suitable Add method, and as

func Add(type T)(a, b T) T { return a + b }

otherwise. Using this Add, we can then write

func Sum(type T)(list []T) T {
	var sum T
	for _, t := range list {
		sum = operators.Add(sum, t)
	}
	return sum
}

We are not proposing that the mechanisms for defining generic functions and their contracts be extended so that any programmer can write specialized versions of functions, such as Add above. Rather, we propose only that the compiler recognize the functions in the operators package and generate specialized instances for those functions, as required.

The proposal

Here we assume generic types and functions have been implemented as in the contracts proposal, with or without the parts relating to contracts.

Package operators

A new package operators will be added to the standard library. The exact name and placement is immaterial; generics/ops might be a suitable alternative, and would have the advantage of a short name.

Functions

Here we list generic functions to be provided in the operators package. For each function, we give a declaration for the function, followed by a list of possible bodies. The intent is that, for specific type arguments, the compiler will choose the first body which compiles successfully. If none of the bodies can be compiled, then the function cannot be used with those type arguments. There is an exception for array types; see below.

func Add(type T)(a, b T) T

  • return a.Add(b)
  • return a + b

func Sub(type T)(a, b T) T

  • return a.Sub(b)
  • return a - b

func Mul(type T)(a, b T) T

  • return a.Mul(b)
  • return a * b

func Div(type T)(a, b T) T

  • return a.Div(b)
  • return a / b

func Rem(type T)(a, b T) T

  • return a.Rem(b)
  • return a % b

func BitAnd(type T)(a, b T) T

  • return a.BitAnd(b)
  • return a & b

func BitOr(type T)(a, b T) T

  • return a.BitOr(b)
  • return a | b

func BitXor(type T)(a, b T) T

  • return a.BitXor(b)
  • return a ^ b

func BitClear(type T)(a, b T) T

  • return a.BitClear(b)
  • return a &^ b

func ShiftLeft(type T)(a T, count uint) T

  • return a.ShiftLeft(count)
  • return a << count

func ShiftRight(type T)(a T, count uint) T

  • return a.ShiftRight(count)
  • return a >> count

func Minus(type T)(a T) T

  • return a.Minus()
  • return -a

func BitComplement(type T)(a T) T

  • return a.BitComplement()
  • return ^a

func Complex(type C, T)(r, i T) C

  • var c C; c.SetRealImag(r, i); return c
  • return complex(r, i)

func Real(type C, T)(c C) T

  • return c.Real()
  • return real(c)

func Imag(type C, T)(c C) T

  • return c.Imag()
  • return imag(c)

func Equal(type T, U)(t T, u U) bool

  • return t.Equal(u)
  • return u.Equal(t)
  • return t == u

func NotEqual(type T, U)(t T, u U) bool

  • return !Equal(t, u)

func Less(type T)(a, b T) bool

  • return a.Less(b)
  • return a < b

func Greater(type T)(a, b T) bool

  • return b.Less(a)
  • return a > b

func LessOrEqual(type T)(a, b T) bool

  • return !b.Less(a)
  • return a <= b

func GreaterOrEqual(type T)(a, b T) bool

  • return !a.Less(b)
  • return a >= b

func Len(type C)(c C) int

  • return c.Len()
  • return len(c)

func Cap(type C)(c C) int

  • return c.Cap()
  • return cap(c)

func At(type C, K, V)(collection C, key K) V

  • return collection.At(key)
  • v, _ := collection.GetAt(key); return v
  • return collection[key]

func GetAt(type C, K, V)(collection C, key K) (V, bool)

  • return collection.GetAt(key)
  • v, ok := collection[key]; return v, ok

func PtrAt(type C, K, V)(collection C, key K) *V

  • return collection.PtrAt(key)
  • return &collection[key]

func SetAt(type C, K, V)(collection C, key K, value V)

  • collection.SetAt(key, value)
  • collection[key] = value

func Delete(type C, K)(collection C, key K)

  • collection.Delete(key)
  • delete(collection, key)

func Slice(type S)(slice S, low, high int) S

  • return slice.Slice(low, high)
  • return slice[low:high]

func SliceCap(type S)(slice S, low, high, max int) S

  • return slice.SliceCap(low, high, max)
  • return slice[low:high:max]

func Append(type S, type T)(slice S, x ...T) S

  • return slice.AppendSlice(x...)
  • return append(slice, x...)

func Make(type T)(n int) T

  • var t T; return t.Make(n)
  • return make(T, n)

func MakeCap(type T)(n, m int) T

  • var t T; return t.Make(n, m)
  • return make(T, n, m)

func Range(type C, K, V)(c C, func f(K, V)) (does not apply to channels)

  • c.Range(f)
  • for k, v := range c { f(k, v) }

func Deref(type P, T)(p P) T

  • return p.Deref()
  • return *p

func Receive(type C, T)(channel C) (T, bool)

  • t, ok := channel.Receive(); return t, ok
  • t, ok := <-channel; return t, ok

func Send(type C, T)(channel C, t T)

  • channel.Send(T)
  • channel <- T

func Close(type C)(channel C)

  • channel.Close()
  • close(channel)

Arrays

Given an array type [n]T, the functions above which can apply to array types should be generated when the first type parameter is *[n]T, but not for [n]T itself. This is for efficiency, and so that functions which alter the container content will alter the caller's version of the array, not a copy local to the function.

This also implies that, given a generic function which calls functions from operators, when calling that function on an array, the & operator must be explicitly used. This is a good thing.

var array [100]int
Sort(array)	// error
Sort(&array)	// ok

Commentary

User-defined types with operators

How should the functions in operators behave with respect to user-defined types which inherit operators from built-in types? For example,

type Int int

Here, Int inherits the + operator from int. The programmer might or might not also define an Add method on Int.

Above, we assume that operators.Add(Int) will use the Int.Add method if it exists. If that method does not exist, the + operator inherited from int will be used. This is probably the best approach, as it allows for operators.Add(Int) to use the + operator with no additional code required, and also allows the programmer to override this choice when necessary.

Another viable rule would be that the inherited + operator is never used. A programmer who wishes to be able to use operators.Add(Int) must define the Int.Add method.

Equality

The Equal function as defined above is not entirely satisfactory. Given types T, U, and V, all of which are supposed to represent integers, we should be able to take a value a of type T and check whether it is equal to a value t of type T, a value u of type U, or a value v of type V. Our Equal probably does not do this unless T, U, and V all implement a common interface I which includes a method Equal(I).

However, it is not obvious how to implement a better Equal, and this one at least covers the important case in which the two type parameters T and U are the same type.

Minor differences

There are some minor differences between the functions above and the built-in operators they encapsulate. These differences should not present any significant problems.

Indices

The At, GetAt, PtrAt, and SetAt functions, when used with a user-defined collection type, will accept only values of the index type in the corresponding methods of the user-defined type. The built-in index expressions, when the collection is a slice or array, will accept indices of any integer type.

type Foo ...
func (f Foo) At(n uint) float64 { ... }
n := 2
var foo Foo
var x float64 = operators.At(foo, n)	// Error: n is not a uint

The PtrAt function cannot be used on an unnamed map type, so as far as the built-in types are concerned, we could restrict the key type to be int. However, it may useful with user-defined collections to allow other key types.

Slice and SliceCap accept only int values as indices; built-in slice expressions accept any integer type.

Shifts

ShiftLeft and ShiftRight accept only uint values as indices; the << and >> operators accept any unsigned integer type.

Comparisons

The comparison functions return a bool value; the built-in operators return an untyped boolean value.

type Bool bool
var b1 Bool = 5 < 7	// ok
var b2 Bool = operators.Less(5, 7)	// Error: cannot assign bool to Bool

For convenience, the comparison functions have been written so that user-defined types need only provide Equal and Less methods. However, ordered comparisons on built-in types still use the corresponding operators. This is because comparisons involving floating point Nans are weird.

Possible Additions

Operator Assignments

We might provide functions which combine a binary operation and an assignment, such as

func AddAssign(type T)(a *T, b T)

  • a.AddAssign(b)
  • *a = (*a).Add(b)
  • *a += b

This would be useful in cases where an AddAssign method can be implemented more efficiently than Add followed by assignment. Note that the first parameter should be a pointer.

Operators as syntactic sugar

We might consider allowing generic functions to use actual operators as syntactic sugar for calls to the corresponding functions in the operators package.

func Sum(type T)(list []T) T {
	var sum T
	for _, t := range list {
		sum = sum + t
	}
	return sum
}

Here, sum + t would generate a call to operators.Add(sum, t). This is probably a really bad idea unless something about Sum, perhaps its contract, explicitly references operators.Sum(T). Even then, it's probably a bad idea.

Zero and One

For types that can undergo forms of addition and multiplication (for example, mathematical arrays), it might be useful to have functions that return the additive and multiplicative identities.

func Zero(type T)() T

  • var t T; return t.Zero()
  • return 0

func One(type T)() T

  • var t T; return t.One()
  • return 1

It might be common to have the zero value represented by Go's normal default initialization, in which case Zero might not be needed. But One might still be needed.

Omissions

Type assertions and type switches

The operators package has no support for these. Actual type assertions and type switches should be used instead.

Function calls

There is no support for building a value which can be called as if it were a function. Use a function literal or method value instead and call it normally.

Logical operators

There is no support for building a user-defined type similar to bool. If you need to do this, it should be sufficient to define a type based on bool, and the built-in operators can then be used.

type MyBool bool

Note also, that while we could trivially provide And, Or, and Not functions corresponding to the &&, ||, and ! operators, And and Or would always evaluate their second argument, unless we defined that argument to be a function returning bool.

Address operator

There is no function corresponding to the unary & operator. This operator already applies to all types, and we should not add confusion.

Conversions

There are no functions to do conversions. The contracts draft does not include methods with extra type parameters in addition to the type parameters of the receiver type. Without those, it is hard to see how to do conversions in the general case.

Unary +

The unary + operator seems to be a no-op, so it has not been included.

Simple assignment

Ordinary assignment statements work for values of any type. While a few types might benefit from having an Assign method that does something more semantically meaningful or more efficient than a simple assignment statement, it is likely that most writers of generic functions will use assignment statements rather an operators.Assign function, even if such were provided. So instead of providing this function, we should encourage writers of types similar to built-in types to ensure that their types work well with ordinary assignment functions.

Select statements

There is no support for select statements. These will only be implemented for actual channel types, not user-defined imitations.

new

There is no function operators.New corresponding to the built-in new function. Instead of new(T), one may write var t T; &t.

Iterating over a collection or channel

The Range function above allows for iterating over the contents of a collection, using a function to describe the action to be taken for each element. For simplicity, there is no way to end the iteration early.

It would also be possible to allow Range to iterate over channels, by providing another possible body:

  • for v := range c { f(struct{}, v }

It is not clear whether this is desirable.

It is also desirable to provide a way to iterate over a collection with finer control, such as

var c C /* C is some collection type */
for r := operators.RangeStart(c); !operators.RangeDone(c, r); operators.RangeNext(c, &r) {
	if !doSomethingWith(r.Key, r.Value) {
		break
	}
}

The return type of operators.RangeStart would depend on the collection type, as some collections may need more than just the current key and value in order to advance to the next position in the collection. In particular, it seems impossible to write suitable versions of RangeStart, RangeDone, and RangeNext as ordinary functions when the collection is a map[K]V.

Contracts

Depending on what form contracts take, if they are added to the language, it might turn out to be impossible to write contracts for the functions in the operators package. This shouldn't be a major problem. The compiler will know what the requirements are, and those requirements can be included in the documentation for programmers to see. Furthermore, these are small, simple functions; future changes should be rare.

When writing a generic function F that calls one of these functions, directly or indirectly, one should include that fact in F's contract. That is, the contract for F should say that it calls operators.Add(T), instead of saying "either T has a method Add or the + operator works on values of type T". The latter is not possible under the current contracts draft.

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