-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: reflect: allow creation of recursive types atomically #39717
Comments
I am continuing my implementation of #39528, and the incompleteness and mutability of the few So I welcome a different API that avoids this problem. Out of curiosity: is there a reason why you did not try to leverage |
I briefly considered it but discarded the idea for the simple reason that I'm not terribly familiar with that API, which, at first sight, seemed unnecessarily complex to me. My interest in the reflect package comes from writing generic pieces of code, and while I might be wrong, I suspect it's similar for most users, so I tried to make the I'm not sure how important that actually is, but another aspect to consider might be the dependencies of the reflect package. I might be misinterpreting this, but it seems to me the implementers went to certain lengths to avoid pulling in "convenience" packages such as |
The reflect package can't depend on go/types, because go/types depends on fmt and fmt depends on reflect. It would be quite awkward to rewrite go/types such that it does not depend on reflect in any way. |
This is certainly a complex API to design. Maybe it would make sense to have a separate package with the same constructors as reflect but that operates only on these incomplete types:
That would at least solve the "Desc everywhere" problem. I'm not sure how much that would end up being a second copy of reflect. And I'm not sure how methods get attached, if anyone wants to try to figure that out. |
An external package that allows building recursive Then the most minimal API is probably: package convert // import "reflect/convert"
import (
"go/types"
"reflect"
)
func ToGoType(reflect.Type) *types.Named
func ToReflect([]*types.Named) []reflect.Type where for simplicity To be implementable, it probably also needs this constraints:
This would allow a user to collect the named Clearly, Such API could even (if it's implementable) create new interface types - something that And it would not need the proposed reflect.Type.Underlying() |
Update: or even package convert // import "reflect/convert"
import (
"go/types"
"reflect"
)
func ToGoType(reflect.Type) types.Type
func ToReflect([]types.Type) []reflect.Type |
@rsc Separate package sounds good. There would certainly be some overlap with existing @cosmos72 your idea to leverage the As for implementation complexity, I guess some stuff would probably have to be moved from |
The idea to leverage
so that's the price to pay for the minimal API: offload the complexity to some other existing package, which does what's needed but also has other features (unused in this case) Any other API which avoids
thus it cannot be much simpler than your proposal or @rsc one. |
Is it not possible to take a middle road, when we still have incomplete types, but only for a defined amount of time. Something like:
This allows the incomplete type to only exist in the closure (as |
If you build on go/types you'd also have to build some kind of bridge between the types already built into the binary and the go/types API. Those are really meant to be separate. It doesn't make sense to cross-connect them just for reflection. Let's focus on non-go/types solutions. Like @cosmos72 said above, it really does seem like the full API (as in #39717 (comment)) is needed, and we just want to get it as lightweight as possible. Maybe someone would be interested to try to build the API sketched in that comment and see how well it works? |
Edit: updated according to remarks by @cosmos72. Attempt at expanding your sketch// Package incomplete implements run-time reflection for incomplete types.
// Its purpose is the creation of recursive types.
//
// Invariants:
//
// Complete([]Type{Of(t)}, nil)[0] == t
// ArrayOf(n, Of(t)) == Of(reflect.ArrayOf(n, t))
// ChanOf(d, Of(t)) == Of(reflect.ChanOf(d, t))
// FuncOf([]Type{Of(in[0]), …, Of(in[len(in)-1])}, []Type{Of(out[0]), …, Of(out[len(out)-1]}, v) == Of(reflect.FuncOf(in, out, v))
// MapOf(Of(k), Of(e)) == Of(reflect.MapOf(k, e))
// PtrTo(Of(e)) == Of(reflect.PtrTo(e))
// SliceOf(Of(e)) == Of(reflect.SliceOf(e))
// StructOf([]StructField{
// StructField{ Name: "Foo", Type: Of(ft) },
// …,
// }) == Of(reflect.StructOf([]reflect.StructField{
// reflect.StructField{ Name: "Foo", Type: ft },
// …,
// })
package incomplete
import "reflect"
// Type represents an incomplete type, or part of an incomplete composite type.
// It is a safe way to define the layout of (possibly recursive) types
// with the Of, NamedOf, ArrayOf, ChanOf, FuncOf, InterfaceOf, MapOf, PtrTo,
// SliceOf, StructOf, and SetUnderlying functions before the actual types are
// created with Complete.
type Type interface {
// interface shared with reflect.Type
ChanDir() reflect.ChanDir
IsVariadic() bool
Len() int
Name() string
NumField() int
NumIn() int
NumMethod() int
NumOut() int
PkgPath() string
// reflect.Type analogous methods
Elem() Type
Field(int) StructField
FieldByName(string) (StructField, bool)
Key() Type
In(int) Type
Method(int) Method
MethodByName(string) (Method, bool)
Out(int) Type
// incomplete.Type-specific methods
// Kind returns the specific kind of this type. It returns reflect.Invalid
// if this type was returned by NamedOf and it has not been defined yet.
Kind() reflect.Kind
// Define defines a named type as the given type. It panics if the type's
// kind is not reflect.Invalid, if the defining type's kind is
// reflect.Invalid, or if the result would contain an invalid recursion.
Define(Type)
// AddMethod adds the given method to this type. It panics if there is a
// method name clash, or if methods with distinct, non-empty PkgPath strings
// are added. Furthermore, one of the following cases must apply:
//
// Case 1: this type was created with InterfaceOf.
//
// Case 2: this type was created with NamedOf and defined to a non-pointer,
// non-interface type.
//
// Case 3: this type was created with PtrTo, with an element type which
// Case 2 applies to.
AddMethod(Method)
// Underlying returns a type's underlying type.
//
// The underlying type of an interface type, an unnamed type, or a type with
// a predeclared name is the type itself. For a user-defined non-interface
// named type, the underlying type is the first unnamed or predeclared type
// in its definition chain. For example, if the type represents A in
//
// type A B
// type B byte
//
// then its underlying type is the type which represents byte (i. e., uint8).
//
// If a named type has not been defined yet, Underlying returns nil.
Underlying() Type
}
// StructField is analogous to reflect.StructField, minus the Offset field.
type StructField struct {
Name, PkgPath string
Type Type
Tag reflect.StructTag
Index []int
Anonymous bool
}
// Method represents an incomplete method.
// Unlike in reflect.Method, the implementing Func is not part of this
// structure.
type Method struct {
Name, PkgPath string
Type Type // receiver = first arg, except for interface methods
}
// Of returns a Type representing the given complete reflect.Type.
func Of(reflect.Type) Type
// NamedOf creates the incomplete type with the specified package path and name.
// The name can be bound to an underlying type with the Define method.
func NamedOf(pkgPath, name string) Type
// ArrayOf creates an incomplete array type with the given count and
// element type described by elem.
func ArrayOf(count int, elem Type) Type
// ChanOf is analogous to reflect.ChanOf.
func ChanOf(dir reflect.ChanDir, elem Type) Type
// FuncOf is analogous to reflect.FuncOf.
func FuncOf(in, out []Type, variadic bool) Type
// InterfaceOf returns an incomplete interface type with the given list of
// named interface types. InterfaceOf panics if one of the given embedded types
// is unnamed or its kind is not reflect.Interface. It also panics if types
// with distinct, non-empty package paths are embedded.
//
// Explicit methods can be added with AddMethod.
func InterfaceOf(embedded []Type) Type
// MapOf creates an incomplete map type with the given key and element types.
func MapOf(key, elem Type) Type
// PtrTo is analogous to reflect.PtrTo.
func PtrTo(t Type) Type
// SliceOf is analogous to reflect.SliceOf.
func SliceOf(t Type) Type
// StructOf is analogous to reflect.StructOf.
func StructOf(fields []StructField) Type
// Complete completes the incomplete types in in, transforming them to a list
// of reflect.Type types. The function method is called once for each method
// added with AddMethod and should return an implementation of that method:
// a function whose first argument is the receiver.
// The list out contains the fully usable resulting types, except that methods
// can be called on them only after Complete has returned. The index indicates
// which type will be the method receiver, and stub indicates the method.
func Complete(
in []Type,
method func(out []reflect.Type, index int, stub Method) interface{},
) []reflect.Type I've used @nrwiersma's idea to add method implementations in the This API is slightly more restrictive compared to what you can do at compile-time, but only as regards order of declarations. For example, given the run-time equivalent of the fragment type A B
func (A) Print() { println("Foo") }
type B int the API would not allow the declaration of |
Thanks for the effort :) Some quick comments:
what about
Please keep the
type Foo struct {
Map *map[Foo]string
} The check on whether
Also, type Foo struct {
Bar Bar
}
type Bar *struct { // notice the '*'
Foo Foo
} |
Thanks for checking my post! @cosmos72 wrote:
I've decided against type A B
type B int but the underlying type of A is then
OK.
Yup, you're correct. I also removed the restriction that you cannot add methods to types which have already been embedded because, e. g., this here is legal: type T struct {
A
}
type A []T
func (A) Foo() {
println("foo")
}
…
T{}.Foo() So yes, we have to delay these checks. Can the restriction not to declare methods on an |
I don't see an obvious reason to replicate |
@ianlancetaylor wrote:
I guess you're talking about most methods of
I intentionally named them "convenience" methods: it's certainly possible to make But using them it will not be pleasant: complicated code will need lots of them, and may even choose to keep them around, to avoid re-creating them every time. I think that making them opaque would often force to maintain the same information externally - duplicating it and risking discrepancies. |
There are two alternatives here:
import "reflect/incomplete"
var A = incomplete.NamedOf("A", "mypackage")
var B = incomplete.NamedOf("B", "mypackage")
B.Define(/*type of int*/)
A.Define(B.Underlying()) // non-trivial dependency: A.Define() must be *after* B.Define() This also requires
import "reflect/incomplete"
var A = incomplete.NamedOf("A", "mypackage")
var B = incomplete.NamedOf("B", "mypackage")
A.Define(B) // any order will do: A.Define() can be before or after B.Define()
B.Define(/*type of int*/) This does not strictly require |
@ianlancetaylor wrote:
I don't think it's strictly necessary. A program which has from the beginning a clear plan from in its mind probably wouldn't interrogate the incomplete types. For something like a Go interpreter, where types may be constructed on the go, it might be convenient. In any case, it looks like a separable problem, so we could start with a minimal API and add interrogation methods later on. FWIW, here is a stripped down version of the API.package incomplete
import "reflect"
// Type represents an incomplete type, or part of an incomplete composite type.
// It is a safe way to define the layout of (possibly recursive) types
// with the Of, NamedOf, ArrayOf, ChanOf, FuncOf, InterfaceOf, MapOf, PtrTo,
// SliceOf, and StructOf functions before the actual types are created with
// Complete.
type Type interface {
// Define defines a named type as the given type. It panics if the type
// is not a named type, the type has already been defined, or if the result
// would contain an invalid recursion.
Define(Type)
// AddMethod adds the given method to this type. The Index field of the given
// method is ignored. It panics if there is a method name clash, or if
// methods with distinct, non-empty PkgPath strings are added. Furthermore,
// one of the following cases must apply:
//
// Case 1: this type was created with InterfaceOf.
//
// Case 2: this type was created with NamedOf and defined to a non-pointer,
// non-interface type.
//
// Case 3: this type was created with PtrTo, with an element type which
// Case 2 applies to.
AddMethod(Method)
}
// StructField is analogous to reflect.StructField, minus the Index and Offset
// fields.
type StructField struct {
Name, PkgPath string
Type Type
Tag reflect.StructTag
Anonymous bool
}
// Method represents an incomplete method.
// Unlike in reflect.Method, the implementing Func is not part of this
// structure.
type Method struct {
Name, PkgPath string
Type Type // receiver = first arg, except for interface methods
Index int
}
// Of returns a Type representing the given complete reflect.Type.
func Of(reflect.Type) Type
// NamedOf creates the incomplete type with the specified name and package path.
// The name can be bound to an underlying type with the Define method.
func NamedOf(name, pkgPath string) Type
// ArrayOf creates an incomplete array type with the given count and
// element type described by elem.
func ArrayOf(count int, elem Type) Type
// ChanOf is analogous to reflect.ChanOf.
func ChanOf(dir reflect.ChanDir, elem Type) Type
// FuncOf is analogous to reflect.FuncOf.
func FuncOf(in, out []Type, variadic bool) Type
// InterfaceOf returns an incomplete interface type with the given list of
// named interface types. InterfaceOf panics if one of the given embedded types
// is unnamed or its kind is not reflect.Interface. It also panics if types
// with distinct, non-empty package paths are embedded.
//
// Explicit methods can be added with AddMethod.
func InterfaceOf(embedded []Type) Type
// MapOf creates an incomplete map type with the given key and element types.
func MapOf(key, elem Type) Type
// PtrTo is analogous to reflect.PtrTo.
func PtrTo(t Type) Type
// SliceOf is analogous to reflect.SliceOf.
func SliceOf(t Type) Type
// StructOf is analogous to reflect.StructOf.
func StructOf(fields []StructField) Type
// Complete completes the incomplete types in in, transforming them to a list
// of reflect.Type types. The function method is called once for each method
// added with AddMethod and should return an implementation of that method:
// a function whose first argument is the receiver.
// The list out contains the fully usable resulting types, except that methods
// can be called on them only after Complete has returned. The index indicates
// which type will be the method receiver, and stub indicates the method.
func Complete(
in []Type,
method func(out []reflect.Type, index int, stub Method) interface{},
) []reflect.Type I've also swapped the order of arguments to @cosmos72: regarding type definitions, I would lean towards your 2., just so the behaviour is not too surprising for users of the API. |
Why would code need them? It seems like it would be enough to just do a sentinel interface like:
Unless you have a specific example of code where those extra methods would be needed? Otherwise the sketch looks about right. Would you like to try implementing it and see if it satisfies your use case? |
This comment has been minimized.
This comment has been minimized.
My use case is a Go interpreter. For this particular case, it can be modeled as a library that takes a slice of arbitrary I tried a mental experiment, and the proposed About implementing |
Hello, as developer of yaegi, also a go interpreter, I'm very interested in having 2 missing features in
To me, a problem is that the pre-compiled code must be able to check that an object instantiated from a such created type implements an existing interface. It seems not trivial to achieve. Also the need to create a new interface type (not existing in the pre-compiled code) is less obvious to me, as only the interpreted code uses it, it can be handled internally in the interpreter (and is already, at least in I understand that the proposed I agree also that this package is for reflect type creation only, not interrogation, as an interpreter has already its own type representation and spec. Once successfully implemented, I don't get yet the interest of keeping this package exposed. It seems to rather belong to internal, and having the interesting functions accessible through But yes definitely interested to participate and help if I can, and put efforts on it. and for info, @nrwiersma an I work in the same team on yaegi. |
Question: The types to move would be at least: |
@cosmos72 Perhaps you can use the already existing package internal/reflectlite. But yes, it would be OK to create a new internal package if required. |
The reason to have the package exposed is that it factors the API out of reflect, where we can use the package name to help shorten the other names. You're right that it would be quite tied up with reflect in the implementation, but it helps keep the concept of incomplete types out of the exported reflect API. As far as methods, the proposed reflect.NamedOf in #16522 has the benefit that it creates a newly named type with methods in one step - by creating and attaching methods in the same operation, it avoids the potential confusion of having a type that does not have any methods at one moment and then does have methods the next. But of course that wouldn't work with recursive types: creating the named type is necessary to finish the recursion. So attaching methods may need to be in this new reflect/incomplete package as well. And of course reflect/incomplete could be used to create non-recursive types as well as recursive types, so it might be that adding methods should be done here. A type that doesn't have methods yet (but will) is incomplete in a different way. I would certainly support exploring that in the API as well. It sounds like in general we think this new package is the right approach, but the devil is in the details. |
I am continuing my implementation at https://github.com/cosmos72/go/tree/issue-39717/src/reflect/incomplete Status: it's not complete yet, but the main skeleton and most details are there.
Comments/suggestions are welcome. |
Background and introduction
The proposal in #39528 has been criticised for the fact that it would introduce "incomplete" instances of
reflect.Type
. This alternative proposal addresses that shortcoming: here, newly created recursivereflect.Type
s can emerge only through a single function,reflect.Bind
. This new function either returns areflect.Type
which can be used with the same expectations as any otherreflect.Type
, or it panics (hence, "atomically"). The remainder of thereflect
API is not affected.This convenience comes at the cost of building a partially parallel infrastructure to describe the type to be newly created. The central component is a new interface
reflect.TypeDesc
instances of which are immutable descriptions of a type (or type-to-be) and a number of functions for the construction of such descriptions.Proposal
We provide the proposal in the form of go source code. For orientation, the following new objects parallel existing objects in the
reflect
package:The following new objects have no parallel:
Proposal as go code:
Open questions
DescribeInterface
function can get an additionalembedded []TypeDesc
parameter. This would allow the creation of interfaces with unexported methods if the embedded interfaces have unexported methods.The text was updated successfully, but these errors were encountered: