Skip to content

Instantly share code, notes, and snippets.

@tom93
Created June 12, 2019 05:33
Show Gist options
  • Save tom93/fd8c500b8a1d2d94f227e1e108d5315b to your computer and use it in GitHub Desktop.
Save tom93/fd8c500b8a1d2d94f227e1e108d5315b to your computer and use it in GitHub Desktop.

Go 2 Generics Feedback

Tom Levy, 2019-06-12

Contents

The zero value

Summary: Add an identifier for the zero value (e.g. zero or _).

I don't like most of the approaches in the draft design to express the zero value:

  • Use var zero T, which works with the existing design but requires an extra statement.

    This is awkward, especially if a function needs to use the zero values of different types (e.g. the Next method of orderedmap.Iterator at the end of the containers example in the draft design).

  • Use *new(T), which is ugly but works with the existing design.

    This is ugly and convoluted.

  • Extend the design to permit using nil as the zero value of any generic type (but see issue 22729).

    It would be surprising if nil could be used with type parameters when they can be set to types that do not support nil. For example func F(type T)() T { return nil } would be valid and and F(string)() would be "", but func F() string { return nil } is not valid. It makes substitution of type parameters (by the compiler, the programmer, or code generators) more complicated, because nil would have to be replaced by the appropriate zero value for the type argument, e.g. "", 0, false, SomeStruct{}.

    Aside: "any generic type" should probably be changed to "any type parameter", because throughout the draft design "generic type" refers to a type that has type parameters, like List(T).

  • Extend the design to permit using T{}, where T is a type parameter, to indicate the zero value of the type.

    This is consistent with struct literals but inconsistent with slice and map literals, because for them {} denotes a sequence of length 0 (which is distinct from the zero value). For example, if S is declared as type S []int then S{} is a slice of length 0, while the zero value of S is a nil slice. If a type parameter T was set to S then T{} and S{} would mean subtly different things, which could be a nasty source of bugs. (Using nil as the zero value of type parameters is better in this regard, because it will either work correctly or cause a compilation error.)

  • Change the language to permit using _ on the right hand of an assignment (including return or a function call) as proposed in issue 19642.

    This would be great. There are also other places where _ would be useful (e.g. comparisons, composite literals, channel send statements) so if this approach is taken I think _ should be permitted everywhere.

I propose another approach: add a predeclared identifier named zero denoting the zero value. It would be like nil, but assignable to all types. The name is consistent with the language spec terminology and the function reflect.Zero().

The zero identifier would overlap with the type-specific zero values (nil, "", 0, etc.). As a matter of style, I suggest favouring the type-specific zero values unless the type is not interesting. This resolves issue 21182 (reduce noise in return statements that contain mostly zero values). The overlap also allows the overloading of nil described in issue 22729 (add kind-specific nil) to be reduced, for example by deprecating nil in favour of zero as the zero value of interface types.

A variant of this approach is to make zero a built-in function such that zero(T) is equivalent to *new(T). This would still solve the problem of zero values for type parameters, but won't help issue 21182.

It's possible to use a library function instead of a built-in function, but that would require adding an empty pair of parentheses because the non-type parameter list must be present:

// usage: Zero(T)()
// e.g. Zero(string)() == ""
func Zero(type T)() T {
	var z T
	return z
}

Restrictions on contract bodies

Summary: Contracts should be able to refer to names from the current package.

The draft design states:

The body of a contract may not refer to any name defined in the current package. This rule is intended to make it harder to accidentally change the meaning of a contract. ... It is likely that this rule will have to be adjusted as we gain more experience with this design. For example, perhaps we should permit contracts to refer to exported names defined in the same package, but not unexported names.

I think this restriction should be lifted (as do Dominik Honnef and Roger Peppe). Here are some concrete arguments against the restriction:

  • Interfaces are similar to contracts and have no such restrictions. They commonly refer to types from the same package with no issues; for example io.WriterTo refers to io.Writer, and io.ReadWriter embeds io.Reader and io.Writer.

  • The restriction means a contract cannot embed another contract defined in the same package. The simple PrintStringer example from the draft design cannot be written in a standalone file or uploaded to the Go Playground because it embeds the contract stringer (which would have to be defined in a separate package). The counters example is illegal because counters embeds counter, and the metrics example is also illegal.

  • The restriction can force the programmer to put related types and contracts in separate packages. This makes accidental changes easier, not harder, because a programmer editing a type is less likely to notice that a contract refers to it when the contract is in a different package.

I also oppose the lenient version where contracts can refer to exported names defined in the same package, but not unexported names:

  • Unexported contracts should be able to refer to unexported names. Generics and contracts can be useful everywhere, including in the unexported part of a package, so all the features should be available to unexported code. Specifically, unexported contracts should be able to embed other unexported contracts and refer to unexported types.

  • Exported contracts should be able to embed unexported contracts. Use case: Factor out the common parts of exported contracts into a separate contract. That contract should be unexported because the precise factoring is an implementation detail which other packages shouldn't depend on. (It's not clear what the generated documentation of the exported contracts should look like, but that is a separate issue which already exists with interfaces.)

  • The interaction of exported and unexported names can be useful in surprising ways, for example exported interfaces with unexported methods lead to "opaque interfaces".

The language guide can advocate that contracts should not refer to names from the current package as a strategy to avoid accidental changes, but I don't think the language spec should dictate this.

Syntax ambiguity with embedded parameterized types

Summary: Parentheses should be permitted around embedded struct fields and embedded interfaces to resolve a syntax ambiguity.

The draft design does not explicitly discuss embedding of parameterized types in structs and interfaces, but presumably it is to be permitted. Such embeddings are syntactically ambiguous. Examples:

type S struct {
	List(int)
	// intention: embedded field of type List(int)
	// Go 1 meaning: field named List of type int
}

type I interface {
	Iterable(int)
	// intention: embed the interface Iterable(int)
	// Go 1 meaning: method named Iterable with one parameter of type int
}

The ambiguity could be resolved by adding parentheses around the types (like in the composite type literal ambiguity), but currently the language spec does not allow parentheses around embedded types in structs and interfaces.


The following sections are still in progress.


Improving type parameter constraints

Issue: Functions and parameterized types can only use a single contract

The draft design allows functions and types to have multiple type parameters, but only a single contract. Non-trivial functions and types are likely to require more than one contract.

For example, a generic function to serialize map[K]V will require that K is comparable, and also that K and V are serializable.

One approach is to write a custom contract for each function:

contract SerializeMapContract(k K, v V) {
	comparable(K)
	Serializable(K); Serializable(V)
}

func SerializeMap(type K, V SerializeMapContract)(m map[K]V, w io.Writer) error {
	...
}

In some situations this approach works well and can help readability, but it would be nice if the function could be written as one unit without an additional top-level declaration for the contract. Also, the function's name is written three times with the naming scheme shown in the snippet.

Issue: Interfaces can't be used directly to constrain type parameters

Go has many existing interfaces, but the draft design does not allow them to be used directly to constrain type parameters. For example, the Stringify function needs a new contract called stringer, but the equivalent interface fmt.Stringer already exists. It is easy to "wrap" an interface in a contract (e.g. contract StringerContract(x T) { fmt.Stringer(x) }), but doing this for all the interfaces that might be used as constraints on type parameters would be horrible.

Proposal: Anonymous contracts

Summary: Allow contracts to be written inline in type parameter lists.

I propose to allow anonymous contracts, much like anonymous functions, structs and interfaces:

func SerializeMap(type K, V contract(k K, v V) {
	comparable(K)
	Serializable(K); Serializable(V)
})(m map[K]V, w io.Writer) error {
	...
}

To reduce verbosity, I also propose that an anonymous contract's parameters can be omitted and inferred from the function's type parameters:

func SerializeMap(type K, V contract {
	comparable(K)
	...

In this version the contract body does not get access to variables of type K and V. If they are required (e.g. to specify methods, operators, or interface conversions) the contract body can introduce a local variable declaration such as var k K or use K(zero) (assuming the zero value proposal is accepted).

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