Go Contracts

Go 2 Contracts

Sept 2018, GoGraz, Lukas Prokop

Go 2017 survey

lack
582 (9.3%)
generics
489 (7.9%)
management
402 (6.5%)
libraries
277 (4.4%)
dependency management
266 (4.3%)
lack of generics
194 (3.1%)
package
159 (2.6%)
gui
137 (2.2%)
library
137 (2.2%)

blog.golang.org/survey2017-results

This question asked for write-in responses. The bars above show the fraction of surveys mentioning common words or phrases. Only words or phrases that appeared in 20 or more surveys are listed

Go 2017 survey

“Is there anything else
you would like to share with us?”

great
130 (2.1%)
generics
119 (1.9%)
love
104 (1.7%)
thank you
104 (1.7%)
thanks
99 (1.6%)
community
87 (1.4%)

Golang 2 features

  • Go Modules
  • check & handle error handling
  • Generics

Let's talk about generics!

Motivation

Resources

Generics?

PRO argument [I heard]:

I don't want to copy-paste code for each type

CONTRA argument [I heard]:

I don't miss generics. Code covered by generics is easily comprehensible

Generics?

Facts:

Many people have concluded (incorrectly) that the Go team’s position is “Go will never have generics.”

Terminology

Generalization based on type parameters was called parametric polymorphism when it was first identified in 1967 […]. The GJ proposal (Java 5) […] changed the terminology first to “genericity” and eventually to “generics”. All imperative languages since Java […] have called it “generics.” We make no distinction between the terms, but it is important to emphasize that “generics” means more than just generic data containers.

## “Contracts — Draft Design” > Russ Cox famously observed that generics require choosing among slow programmers, slow compilers, or slow execution times.
## Example 1 ``` package main import ( "fmt" "math" ) func max32(vals []float32) float32 { max := float32(-math.MaxFloat32) for _, val := range vals { if val > max { max = val } } return max } func max64(vals []float64) float64 { max := -math.MaxFloat64 for _, val := range vals { if val > max { max = val } } return max } func main() { fmt.Println(max32([]float32{3.141592, 2.718281828})) fmt.Println(max64([]float64{3.141592, 2.718281828})) } ``` *Status quo:* copy & paste
## Example 2 ``` package main import ( "fmt" "math" ) func max32(vals []float32) float32 { max := float32(-math.MaxFloat32) for _, val := range vals { if val > max { max = val } } return max } func max64(vals []float64) float64 { max := vals[0] for i := 1; i < len(vals); i++ { max = math.Max(max, vals[i]) } return max } func main() { fmt.Println(max32([]float32{3.141592, 2.718281828})) fmt.Println(max64([]float64{3.141592, 2.718281828})) } ``` Use stdlib. Use `math.Max` in `max64`.
## Example 3 https://golang.org/pkg/sort/ * type Float64Slice * Len() int * Less(i, j int) bool […] * type IntSlice * Len() int * Less(i, j int) bool […] * type StringSlice * Len() int * Less(i, j int) bool […]
## Example 4 ``` func Print(type T)(s []T) { for _, v := range s { fmt.Println(v) } } Print(int)([]int{1, 2, 3}) ``` *Parametric polymorphism* in Go. Use `()` to specify the type.
## Contract > This is an important rule that we believe should apply to any attempt to define generic programming in Go: there should be an explicit contract between the generic code and calling code. ``` func Stringify(type T)(s []T) (ret []string) { for _, v := range s { ret = append(ret, v.String()) // INVALID } // does T support String()? return ret } ```
## Contract Multiparameter example: ``` func Print2(type T1, T2)(s1 []T1, s2 []T2) { ... } ``` > Although functions may have multiple type parameters, they may only have a single contract.
## contract example > A contract is introduced with a new keyword contract. The definition of a contract looks like the definition of a function, except that the parameter types must be simple identifiers.
## contract example ``` contract stringer(x T) { var s string = x.String() } ``` > Contracts may only appear at the top level of a package. > While contracts could be defined to work within the body of a function, it’s hard to think of realistic examples in which they would be useful
## Contract [fixed] ``` func Stringify(type T stringer)(s []T) (ret []string) { for _, v := range s { ret = append(ret, v.String()) // now valid } return ret } ``` Recognize `stringer` in `type T stringer`!
## convertible ``` contract convertible(_ To, f From) { To(f) } ``` A contract enforcing that some type `From` is convertible to some type `To`. `(int64, int32)` is valid. `([]int, complex64)` is not. ``` func FormatUnsigned(type T convertible(uint64, T))(v T) string { return strconv.FormatUint(uint64(v), 10) } s := FormatUnsigned(rune)('a') ```
## contract bodies > The body of a contract may not refer to any name defined in the current package. A contract is permitted to refer to names imported from other packages. […] Contract bodies are not executed. ``` contract Readable(r T) { io.Reader(r) } ```
## Parameterized types > We want more than just generic functions: we also want generic types. > We suggest that types be extended to take type parameters. ``` type Vector(type Element) []Element ```
## Parameterized type(s| aliases) > To use a parameterized type, you must supply type arguments. This is called instantiation. ``` var v Vector(int) ``` > Type aliases may have parameters. ``` type Ptr(type Target) = *Target ```
## Parameterized type aliases > Type aliases may refer to parameterized types, in which case any uses of the type alias (other than in another type alias declaration) must provide type arguments. ``` type V = Vector var v2 V(int) ```
## Parameterized type aliases > Type aliases may refer to instantiated types. ``` type VectorInt = Vector(int) ```
## Methods with additional types > Although methods of a parameterized type may use the type’s parameters, methods may not themselves have type parameters.People will have to write a top-level function where it would be useful. Might be added later.
## Contract embedding A contract may embed another contract, by listing it in the contract body with type arguments. This contract embeds the contract stringer defined earlier. ``` contract PrintStringer(x X) { stringer(X) x.Print() } ```
## Self-referential types ``` package comparable contract equal(v T) { var x bool = v.Equal(v) } ```
## Self-ref. types usage ``` func Index(type T equal)(s []T, e T) int { for i, v := range s { if e.Equal(v) { return i } } return -1 } ``` ``` import "comparable" type EqualInt int // The Equal method lets EqualInt implement // the comparable.equal contract. func (a EqualInt) Equal(b EqualInt) bool { return a == b } func Index(s []EqualInt, e EqualInt) int { return comparable.Index(EqualInt)(s, e) } ```
## Power of contracts They allow to combine types of various packages. > The ability for type parameters to refer to other type parameters illustrates an important point: it should be a requirement for any attempt to add generics to Go that it be possible to instantiate generic code with multiple type arguments that refer to each other in ways that the compiler can check.
## Function argument type inference ``` Print(int)([]int{1, 2, 3}) ``` Why specify int? It's already there. ``` Print([]int{1, 2, 3}) ``` Type unification is applied to derive types. Type inference is a convenience feature.
## Instantiating a function > Go normally permits you to refer to a function without passing any arguments, producing a value of function type. > > You may not do this with a function that has type parameters; all type arguments must be known at compile time.
## Instantiating a function > However, you can instantiate the function, by passing type arguments, without passing any non-type arguments. This will produce an ordinary function value with no type parameters. ``` // PrintInts will have type func([]int). var PrintInts = Print(int) ```
## Parser ambiguity ``` x1 := []T(v1) // type conversion? partial type instantiation? x2 := []T(v2){} // composite literal of type []T(v2) which is …? x3 := []T(v1) // always type conversion x4 := [](T(v1)) // always partial type instantiation ```
## Operator overloading? ``` func Contains(type T)(s []T, e T) bool { for _, v := range s { if v == e { // INVALID return true } } return false } ```
## Operator overloading in contracts ``` contract comparable(x T) { x == x } func Contains(type T comparable)(s []T, e T) bool { for _, v := range s { if v == e { // now valid return true } } return false } ```
## Auto operator overloading * `a * b` implies `a *= b` * `==` iff `!=` * `<` implies `==`, `!=`, `<`, `<=`, `>=`, and `>`. * `>` implies analogously ``` contract convert(t To, f From) { To(f) From(t) f == f } ``` To(f) does not imply From(t)
## Impossible contracts > It is possible to write a contract body that cannot be implemented by any Go type. This is not forbidden. An error will be reported not when the contract or function or type is compiled, but on any attempt to instantiate it.
## Pervasiveness > We expect that a few new packages will be added to the standard library. A new slices packages will be similar to the existing `bytes` and `strings` packages, operating on slices of any element type. New maps and chans packages will provide simple algorithms that are currently duplicated for each element type. A `set` package may be added.
## Pervasiveness > Packages like `container/list` and `container/ring`, and types like `sync.Map`, will be updated to be compile-time type-safe. > > The `math` package will be extended to provide a set of simple standard algorithms for all numeric types, such as the ever popular `Min` and `Max` functions.
## Pervasiveness > It is likely that new special purpose compile-time type-safe container types will be developed, and some may become widely used. > > We do not expect approaches like the C++ STL iterator types to become widely used. In Go that sort of idea is more naturally expressed using an interface type
## Discussion * There is no way to specify that a method does not return any values. * There is no way to specify that a method takes variadic arguments. * There is no way to distinguish a method call from a call of a struct field with function type. * No specialization. No way to write multiple versions of a generic function that are designed to work with specific type arguments (other than using type assertions or type switches).
## Discussion * No metaprogramming. There is no way to write code that is executed at compile time to generate code to be executed at run time. * No higher-level abstraction. There is no way to speak about a function with type arguments other than to call it or instantiate it. There is no way to speak about a parameterized type other than to instantiate it. * No covariance or contravariance.
## Discussion * No operator methods. You can write a generic container that is compile-time type-safe, but you can only access it with ordinary methods, not with syntax like `c[k]`. Similarly, there is no way to use range with a generic container type. * No currying. There is no way to specify only some of the type arguments, other than by using a type alias or a helper function.
## Discussion * No adaptors. There is no way for a contract to define adaptors that could be used to support type arguments that do not already satisfy the contract, such as, for example, defining an `==` operator in terms of an `Equal` method. * No parameterization on non-type values. Arises for arrays, where it might sometimes be convenient to write type `Matrix(type n int) [n][n]float64`. It might also sometimes be useful to specify significant values for a container type, such as a default value for elements.
## Disclaimer > This design is not perfect, and it will be changed as we gain experience with it. That said, there are many ideas that we’ve already considered in detail. This section lists some of those ideas in the hopes that it will help to reduce repetitive discussion. The ideas are presented in the form of a FAQ.
## IMHO * constrained concept to introduce generics for major usecases * fits into Go language design * I don't like () for type parameterization, most people are used to <> these days * contract bodies are open-ended and allow easy extensibility in the future * It will take some time to understand how to write beautiful™ contract bodies
## Final remarks > We experimented with other syntaxes, such as using a colon to separate the type arguments from the regular arguments. The current design seems to be the best, but perhaps something better is possible.
## Final remarks > When parsing code within a function, such as `v := F<T>`, at the point of seeing the `<` it’s ambiguous whether we are seeing a type instantiation or an expression using the < operator. Resolving that requires effectively unbounded lookahead. In general we strive to keep the Go parser simple.

Thanks!

😀