Skip to content

Proposal: allow conversion between recursively equivalent types #18030

@adrianludwin

Description

@adrianludwin

Summary: I propose we consider relaxing struct conversion rules so that when converting composite data types (eg structs and slices), we recursively attempt to convert element types as well as the fundamental types. This would simplify certain important use cases, such as the handling of (otherwise) identical protobuf messages.

1. Background: This proposal is inspired by the (accepted) proposal described in #16085, and heavily references that proposal, but has a significantly wider scope. For example, under the current type conversion rules, we can convert between the following two types:

type type1 struct {
	id int
}

type type2 struct {
	id int
}

But we cannot convert between the following two types, even though they are clearly identical, from a data perspective:

type id1 int
type type1 struct {
	id id1
}

type id2 int
type type2 struct {
	id id2
}

Unfortunately, this is very similar to the kinds of structures that are created by the Protocol Buffer compiler. This means that many structurally identical messages cannot easily be converted from one proto package to another.

Issue #16085 (Section 5) did specify that tags should be ignored recursively, for the purposes of type conversion, but this is only relevant for anonymous structs and therefore is not widely applicable. However, it does foreshadow the issues raised by the this proposal.

2. Proposal: The current spec defines that “A non-constant value x can be converted to type T [if, among other cases,] x's type and T have identical underlying types.” Instead, I suggest that this be changed to “x’s type and T have structurally equal (SE) types.” As one might expect, two types are SE if they meet any of the current criteria (including being of the same underlying type), or if they:

  • Have the same Kind (as defined in the “reflect” package), and
  • Have component types that are themselves SE.

To put this in concrete terms for each of Go's composite types:

  • Array, channel or slice: two array, channel or slice types are SE if their element types are SE. Of course, an array type is not SE with a channel or slice type or vice versa.
  • Functions and methods: two function types are SE if all the following conditions are met:
    • The corresponding parameters (by order of declaration) of the two functions are identically named and have SE types;
    • The corresponding return values (by order of declaration) of the two functions are either both unnamed or identically named, and have SE types.
  • Interfaces: two interface types are SE if they have the identical set of methods, and all corresponding methods of the same name are SE.
  • Map: two map types are SE if the keys and values are SE.
  • Pointer: two pointer types are SE if their base types are SE (this is already allowed by the spec, but as a special case).
  • Struct: two structs are SE if the corresponding fields (by order of declaration) of the two structs are identically named and have SE types.

In addition to changing the built-in language behaviour, we would also update the reflect package to match this change (Type.ConvertibleTo and Value.Convert).

3. Impact: as with #16085, this is a backwards-compatible change since it only loosens restrictions. The only exceptions to this are the behaviour of several methods from the “reflect” package, as described in #16085. Furthermore, the benefits should be greatly enhanced when dealing with protobufs, as described in the background.

4. Discussion: While the primary driver for this change was to simplify the handling of structures, I have included functions, methods and interfaces for completeness. However, if the inclusion of these functional elements causes problems, it may be acceptable to remove them from this proposal as well.

A further note should be made about method sets. Under the current rules, it is already possible to cast one type to another and completely change its method sets. As a result of the current proposal, the method sets of its element types may also change. For example, if x.a.Foo() was a valid call, and x is casted to y (a variable of a different but SE type to x), this does not necessarily mean that y.a.Foo() will be a valid call - or if it is a valid call, that it will refer to a method with the same signature, let alone semantics.

5. Alternatives: One alternative to allowing automatic conversion is for developers to continue to manually specify every field that needs to be converted. Another is to provide a library function, which would fail at runtime (instead of at compile time) if the two types were not SE. There would also be performance and memory considerations, as in #16085.

6. Implementation: TBD. In the compiler, we would need to generalize the type identity functions to recurse in the case of conversions. The spec would be updated with a new section on structural equality, and the reflect package would need to be updated to match the behaviour of the compiler.

Thanks to @griesemer for some preliminary comments on this proposal.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions