golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.8k stars 17.51k forks source link

proposal: spec: relax structural matching for conversions #19778

Open luna-duclos opened 7 years ago

luna-duclos commented 7 years ago

Currently, as of Go 1.8, one can convert between two identical types, A and B, defined as follows:

type A struct {
    Foo int
}

type B struct {
    Foo int
}

It is however not possible to convert between []A and []B, even though the memory layout of both slices is identical, and one has to resort to following hackery to get both compile time checking and O(1) conversion time:

package main

import (
    "fmt"
    "unsafe"
)

type A struct {
    Foo int
}

type B struct {
    Foo int
}

// Compile time check that A and B are the same
var _ = A(B{})

func main() {
    a := A{Foo: 5}
    fmt.Println(B(a))

    as := []A{{Foo: 5}}
    fmt.Println(*(*[]B)(unsafe.Pointer(&as)))
}

This is often useful when different parts of an application are responsible for different parts of the handling of the lifetime of an object. For example, a database layer could return []A, which would be a type with db:"foo" decorators to help with database deserialization using the sqlx package, while the http handler wants to use []B, because it contains the field tags necessary for json serialization.

Currently, a copy or unsafe is required, I propose allowing slices with identical memory layouts to be converted between another, and to restrict this to only 1 level deep to reduce possible complexity in the compiler.

This is a similar reasoning why the spec was changed to allow structs with different field tags to be converted between eachother, and I believe it would help avoid usage of unsafe and copies in many applications which seperate out concerns between different layers.

griesemer commented 7 years ago

cc: @griesemer

ericlagergren commented 7 years ago

This is something I've always found a little counterintuitive, especially if the underlying types are the same. I can think of a couple of times I've had to write my own strings.Join routines because the alternative is to copy the entire slice. e.g., https://github.com/ericlagergren/decimal/blob/522450d1e655d86ef13b0bfad42d090a0240ce03/suite/suite.go#L73

It seems to make sense that if T1 and T2 are slices whose elements are all the same underlying types, they should be convertible without an allocation.

griesemer commented 7 years ago

@luna-duclos Note that for struct field tags, conversion is ignoring the tags "all the way down" - not just one level deep. That would be something to consider if we were to make this change. (Though this is not going to happen for Go 1.0 I suspect).

pebbe commented 7 years ago

This goes for non-struct types as wel. If you have this:

type MyType int

You can't do this:

a := []int{1, 2, 3}
b := []MyType(a)

Instead, you have to do this:

a := []int{1, 2, 3}
b := make([]MyType, len(a))
for i, v := range a {
    b[i] = MyType(v)
}

Making b := []MyType(a) possible would perhaps eliminate the desire for generics in some cases?

ianlancetaylor commented 6 years ago

Seems related to #15209, which is about append and copy.

ianlancetaylor commented 6 years ago

As I understand it, I think this proposal is asking for a concept that the language does not currently have: we permit converting []A to []B when A and B have identical underlying types ignoring field tags and type names, recursively (and this actually applies to all composite types, not just slices).

That can't really be the rule, though, because it breaks for interface types. Consider

type X int
type Y1 interface { M(X) }
type Y2 interface { M(int) }
var V1 []Y1
var V2 []Y2

Can I write V2 = ([]Y2)(V1)? According to the rule I wrote above, I can, because the interface types are identical ignoring type names. But this conversion can't be right, because the set of types that implement Y1 is disjoint from the set of types that implement Y2.

So the conversion rule must be something more complex than I wrote above. What is it?

vvoronin commented 6 years ago

It can be done by adding a type convertion check along with underlying type equality. If we formulate the rule like this: "permit converting composite types like []A to []B when A and B have identical underlying types ignoring field tags and type names and can be type converted, recursively"

gopherbot commented 6 years ago

Change https://golang.org/cl/105937 mentions this issue: reflect: prevent conversion of invariant slices

ianlancetaylor commented 6 years ago

@vvoronin Thanks, but I'm not sure I understand your adjusted rule. Clearly it can't apply at top level, since at top level the types can not be converted. So, when does it apply?

vvoronin commented 6 years ago

@ianlancetaylor Oh, you are right, sorry for inconvenience, my english is quite bad.

I meant "when simple non-composite type can be type converted and have have identical memory layout, composite of that type also must be able to be type converted"

For example:

type A struct {   A string  }
type B struct {   A string  }
// we can convert this
var varA A = A(B{})
//  and A and B have identical memory layout, so
var sliceOfB []B = []B([]A{})
type Y1 interface { M(X) }
type Y2 interface { M(int) }
var V1 Y1
var V2 Y2
// we cannot convert this
V2 = Y2(V1)
// so we cannot convert a slice or map of this
// we can convert this
byteArray := []byte(`string`)
// but memory layout is different
// so we cannot convert a slice or map of this