Tom Levy, 2019-06-12
-
The zero value
Summary: Add an identifier for the zero value (e.g.zero
or_
). -
Restrictions on contract bodies
Summary: Contracts should be able to refer to names from the current package. -
Syntax ambiguity with embedded parameterized types
Summary: Parentheses should be permitted around embedded struct fields and embedded interfaces to resolve a syntax ambiguity.
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 oforderedmap.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 supportnil
. For examplefunc F(type T)() T { return nil }
would be valid and andF(string)()
would be""
, butfunc F() string { return nil }
is not valid. It makes substitution of type parameters (by the compiler, the programmer, or code generators) more complicated, becausenil
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{}
, whereT
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, ifS
is declared astype S []int
thenS{}
is a slice of length 0, while the zero value ofS
is anil
slice. If a type parameterT
was set toS
thenT{}
andS{}
would mean subtly different things, which could be a nasty source of bugs. (Usingnil
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
}
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 toio.Writer
, andio.ReadWriter
embedsio.Reader
andio.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 becausecounters
embedscounter
, 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.
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.
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.
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.
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).