Skip to content

proposal: reflect: add generic type arg info to reflect.Type #54393

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

Open
aaronc opened this issue Aug 11, 2022 · 19 comments
Open

proposal: reflect: add generic type arg info to reflect.Type #54393

aaronc opened this issue Aug 11, 2022 · 19 comments
Labels
Milestone

Comments

@aaronc
Copy link

aaronc commented Aug 11, 2022

I'm not sure if this has been discussed already (I couldn't find a prior issue), but currently reflect.Type has no direct methods to retrieve the type arguments of generic types.

The only way to retrieve these type arguments is by parsing the string returned by Type.Name() which includes fully qualified package names and could look something like this in a complex case: MyGenericType[some/package.Foo, map[string]other/package.Bar]. This string is not easy to parse because of the possibility of nested type arguments, maps, slices, etc.

I propose adding methods to reflect.Type to retrieve these type arguments programmatically:

NumTypeArg() int
TypeArg(i int) Type
@gopherbot gopherbot added this to the Proposal milestone Aug 11, 2022
@aaronc aaronc changed the title proposal: reflect: add generic type param info proposal: reflect: add generic type param info to reflect.Type Aug 11, 2022
@ianlancetaylor
Copy link
Member

I'm not sure TypeParam is the right name here. The type parameters are the parameters that appear in the type definition. I think that what you are describing is what we usually call the type arguments.

@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Aug 11, 2022
@robpike
Copy link
Contributor

robpike commented Aug 11, 2022

@ianlancetaylor Those terms are not the ones I know from my youth. We used to talk about formals and actuals, which carry a distinct meaning already. Modern computing's use of the terms parameters and arguments results in a domain-specific redefinition of true synonyms in normal English. However, I admit that redefinition is not unique to Go.

I wonder why this terminology shift happened.

@icholy
Copy link

icholy commented Aug 15, 2022

Adding methods to reflect.Type would be a breaking change.

@zephyrtronium
Copy link
Contributor

@icholy reflect.Type has unexported methods, so every type which implements it is in package reflect and can be updated along with the interface. Is there another sense in which it would be a breaking change to add methods to reflect.Type?

@ianlancetaylor
Copy link
Member

@robpike That's true, now that you mention that I remember "formal" and "actual" as well. But today even the Go spec speaks of "function parameters" and "function arguments".

@apparentlymart
Copy link

apparentlymart commented Sep 1, 2022

FWIW I also learned "formal parameters" in school but have tended to use "parameter" and "argument" in my writing for at least the last decade or so because that seems (anecdotally) to be the current familiar jargon across various different language specs and tutorials.

It is unfortunate that in plain English "parameter" and "argument" are not clearly differentiated in the way that is intended in this context, but that seems to be a common characteristic of plain words adopted as jargon. The dictionary tells me that "formal" as a noun means "an evening gown" and that "actual" isn't a noun at all, so those words don't seem to be obviously better cognates. I think it's beneficial to go with the flow here and use terms that people are likely to have encountered in other languages and in tutorials. (even though this sort of jargon evolution does make me notice my age.)

With the obvious caveat that Wikipedia is a tertiary source rather than an authority, I do note that Parameter (computer programming) mentions both pairs of terms, but gives priority to "parameter" and "argument" while relegating "formal argument" and "actual argument" to secondary position. With that said, the section Parameters and Arguments does go on to acknowledge the inconsistency of usage and potential confusion between them.


Interestingly, one of the first tutorials I found when looking for examples -- the "Functions" section of A Tour Of Go -- seems at first read to be using these terms without defining them and switching somewhat carelessly from one to the other without explaining their relationship:

A function can take zero or more arguments.

In this example, add takes two parameters of type int.

As a reader who already knows the common meanings of "arguments" vs. "parameters" I probably wouldn't noticed this if I wasn't explicitly looking for examples. Perhaps what we can learn from this example is that the terms "argument" and "parameters" are so familiar to programming language learners that explicit definition and distinguishing remarks felt unnecessary here. (I'm assuming that a non-trivial number of people have successfully learned Go in part by following this tour.)

@atdiar
Copy link

atdiar commented Sep 1, 2022

Seems about the same difference between variables and values to me.
Parameter is to variable what argument is to value.

@aaronc
Copy link
Author

aaronc commented Sep 1, 2022

Happy to switch this proposal to TypeArgument, TypeArg or even TypeActual. What do people prefer?

Does the proposal otherwise sound reasonable?

@icholy
Copy link

icholy commented Sep 1, 2022

It should probably be

NumTypeArg() int
TypeArg(i int) Type

@aaronc aaronc changed the title proposal: reflect: add generic type param info to reflect.Type proposal: reflect: add generic type arg info to reflect.Type Sep 1, 2022
@aaronc
Copy link
Author

aaronc commented Sep 1, 2022

I've changed this proposal to use TypeArg for now. That seems reasonable. Happy to change to a different naming if people prefers otherwise.

@icholy
Copy link

icholy commented Sep 1, 2022

@aaronc NumTypeArgs should be NumTypeArg to match the rest of the relect.Type methods.

@aaronc
Copy link
Author

aaronc commented Sep 1, 2022

@aaronc NumTypeArgs should be NumTypeArg to match the rest of the relect.Type methods.

Updated

@mdempsky
Copy link
Contributor

Note that if you have a local defined type declared within a type-parameterized function, then that function's type parameters are also implicit type parameters of the defined type.

For example:

func F[X any]() any {
    type T int
    return T(0)
}

then F[X].T is implicitly parameterized by X.

@jonbodner
Copy link

Is there any chance this issue will be revisited?

@ianlancetaylor
Copy link
Member

@jonbodner This proposal remains on the incoming queue.

@adonovan
Copy link
Member

BTW, the "formal" vs "actual" parameter terminology dates (as so many things do) from ALGOL, and is still widely used in academic work, but a wide variety of language communities seem to have standardized on the "parameter"/"argument" terminology.

@LucDrenth
Copy link

This looks like a good proposal and no criticism has been given against it. Can this proposal be moved to the next phase?

@apparentlymart
Copy link

apparentlymart commented Apr 24, 2025

We spent a bunch of time discussing the surface API shape, and it seems that landed in a good place where there's consensus. However, we haven't yet discussed implementation tradeoffs.

I'm far from an expert on this topic and so hopefully someone will correct me if I'm wrong on this 🤞 but I believe that currently only the compiler actually tracks the individual arguments for an instantiated generic type, while the final compiled program only has the string that's returned by Type.Name(), like the MyGenericType[some/package.Foo, map[string]other/package.Bar] string described in the original propsal.

That would mean that we'd also need to change the type information data structures to somehow track the type parameters in a way that allows recovering suitable Type objects for them.

Perhaps that means expanding abi.Type to have an additional field tracking an optional offset for some data retained elsewhere in the type information block, which would consist of a length followed by the specified number of TypeOff values. This optional offset could then be zero for a type that is not based on a generic type, so the overhead in that case is just one additional int32 value to store for each type.

Perhaps we could get extra fancy and add a new flag to TFlag whose presence redefines the Str NameOff field to be the offset of a variable-length structure containing both the type name and the array of TypeOff values instead of just the name, and then that flag could be set only for types that are based on generic types and the size of non-generic types would be unchanged. (TFlag seems to have three bits left unused, so there is room for this but of course these precious few bits might be better reserved for something else.)

Could the toolchain test whether there's any potential call to Type.NumTypeArg or Type.TypeArg anywhere in the program and skip including any new information at all if not? In that case, I imagine that a program which definitely uses neither of those functions would never set the new bit in TFlag, and so there'd be no size increase at all.

My intent in all of the above is to try to consider the cost of accepting this proposal for programs that would not actually use it, since I assume that only a small number of total Go programs need to inspect generic type parameters at runtime:

  • Growing the type information size for all types would be very unfortunate, and probably unacceptable?
  • Growing the type information size only for types based on generic types is better.
  • Growing the type information size only for types based on generic types and only when the program actually contains a call to Type.NumTypeArg/Type.TypeArg seems ideal, assuming my assumption is correct that most programs would not include any such calls.

@apparentlymart
Copy link

apparentlymart commented Apr 24, 2025

My previous comment was trying to understand the cost of accepting this proposal.

I also note that this issue doesn't include much discussion of the benefit of accepting the proposal. What kind of program needs to be able to inspect the type information for generic type arguments at runtime?

I have previously written code1 where it was useful to dynamically detect a type argument for one specific generic type, but that's already possible to solve in at least two different ways today, depending on how much information you have at compile time:

type Example[T any] struct {
	Value T
}

func exampleArgType[T any](v Example[T]) reflect.Type {
	return reflect.TypeOf(T)
}
type Example[T any] interface {
	DoThing(T)
}

// exampleArgType takes a value whose dynamic type implements
// some instantiation of Example[T] and returns which T it implements
// it with. Results are unspecified if the given value does not implement
// any instantiation of Example[T].
func exampleArgType(v any) reflect.Type {
	outer := reflect.TypeOf(v)
	method := outer.MethodByName("DoThing")
	return method.Type.In(0)
}

It seems like this proposal would only be needed for situations where the program using this API knows nothing at all about the types it's going to be working with at runtime. Can anyone share concrete examples of such programs that would be useful to be able to write?

Footnotes

  1. (A realistic example)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Incoming
Development

No branches or pull requests