Skip to content
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: spec: static field (and type) references #7112

Closed
gopherbot opened this issue Jan 13, 2014 · 8 comments
Closed

proposal: spec: static field (and type) references #7112

gopherbot opened this issue Jan 13, 2014 · 8 comments
Labels
FrozenDueToAge LanguageChange v2 A language change or incompatible library change
Milestone

Comments

@gopherbot
Copy link

by rasmus@mindplay.dk:

For the purposes of (primarily) reflective meta-programming, Go needs a syntax for
static/lexical field (and possibly type) references - that is, some sort of value
representation of "struct.field", such as is required and implemented by
unsafe.ArbitraryType, as used by the Alignof() and Offsetof() functions:

http://godoc.org/unsafe

It is currently only possible to reference a type indirectly, by creating an empty
instance of a value and using reflection, e.g.:

    t := reflect.TypeOf(MyType{})

This is probably acceptable in most cases, as you can typically do this once, in a
package, and have your reflect.Type value initialized and available without repeatedly
creating and throwing away garbage values.

As for fields, the problem is worse - the only means of obtaining a value representing a
field are awkward, complicated, and unreliable.

The simplest and most obvious approach, is to reference the field by name, using a
string:

    type Foo struct {
        Bar string
    }

    t := reflect.TypeOf(Foo{})
    
    f, _ := t.FieldByName("Bar")

The name "Bar" can't be checked at compile-time, but has to be checked at
run-time - since it is actually referencing a source-code element, it should be
checkable at compile-time, but there is no syntax to do so.

One of the more complicated solutions I came up with, involves reflection, measuring the
distance between the start of a parent struct and it's field, and searching for the
struct.field with the determined offset:

http://play.golang.org/p/-geaQaj7Ee

This is "type safe" at compile-time, to the extent that this is currently
possible, and with some limitations; for one, establishing a direct connection between
the parent struct and the field, is not possible - which means you could pass a struct
of a different type (or a different instance of the right type) and a field that doesn't
match, and you would get a run-time panic.

For the purposes of reflective meta-programming, being able to statically reference
types and fields would be invaluable to me. Go provides annotations ("tags")
for fields, but does not provide any way to directly select one of these fields, in a
statically safe type-checked manner; which means that tags are only useful for tasks
where iterating over the entire structure and inspecting all the fields is necessary
and/or possible - pin pointing an exact field, e.g. for the purposes of extracting it's
tag, is not currently possible; or at least not in a simple, reliable, type-safe way.

My first impulse when attempting to reference a type or field as a value, was something
like this:

    type Foo struct {
        Bar string
    }

    t := Foo
    f := Foo.Bar

Of course, that doesn't mean anything at all, and doesn't work.

I'm proposing one of three solutions to this shortcoming. I'm not a language architect,
so I have no real bias towards one or the other - these are just the three ways I could
think of.

One would be to actually make the above examples mean something - so that, when Foo is
assigned to variable, there is a resulting implicit type representing that specific type
(call it 'Foo for the sake of argument), e.g. for a defined type:

    type struct Foo {
        Bar string
        Baz int
    }

There is an implied type: (psuedo code)

    type struct 'Foo {
        'Name string
        'Pkg string
        Bar Field
        Baz Field
    }

Which would enable you to refer to types directly:

    f := Foo.Bar // Field

The Field type in this example is some lightweight, built-in type containing a reference
to the parent 'Foo type - the 'Foo type itself contains the package and type-name as
strings, so we can use reflection to obtain information about the type.

The second approach would be an actual operator used to reference types or fields, e.g.:

    t := 'Foo // 'Foo
    f := 'Foo.Bar // Field

This approach is similar to the first, but more explicit - types and members do not have
a direct, literal value; you have to "ask" for it.

This could also (optionally) work for locally typed variables, e.g.:

    v := Foo{}
    t := 'v // 'Foo
    f := 'v.Bar // Field

The last approach I could think of is very different from the two first, and involves
introducing some kind of indirection operator - for the sake of argument, I'm using the
apostrophe as the operator in this example:

    v := Foo{Bar: "Hello"}
    f := 'v.Bar // FieldRef
    
    // indirectly reading the value:
    fmt.Sprintf("%v", f.Get() === v.Bar) // "true"
    
    // indirectly writing the value:
    f.Set("World")
    fmt.Sprintf("%v", v.Bar === "World") // "true"

In this case, a new type FieldRef is introduced - it contains information like the
package and type-name of the struct, a pointer to the actual struct, and the name (or
index) of the referenced field within that struct. It provides methods to read/write
(without type-safety, as per reflection) the value of the referenced member.

In other words, the big difference from the first two examples, is that this approach
lets you statically reference a struct.field with compile-time type-safety, while also
referencing an actual value - in the two first examples, the reference to a type/field
is explicit, while in the last case, the type/field is picked up from a (locally typed)
variable.

As pointed out by others (on the forums) this approach may have less general use, and
could "grow out of control", since it is applies to a very specific use-case.

As stated, I am not a language architect, and several people did point out various
drawbacks to all of these approaches. Refer to the discussion here for more details:

https://groups.google.com/forum/#!topic/golang-nuts/69B5ZUUNHbM
@ianlancetaylor
Copy link
Contributor

Comment 1:

Any of these options would make Go a far more dynamic language, and as such they are
unlikely to be implemented.  If you want to seriously propose them you need to consider
how they behave for every type and every expression, not just structs and fields of
structs.

Labels changed: added repo-main, release-none, languagechange.

@gopherbot
Copy link
Author

Comment 2 by rasmus@mindplay.dk:

> Any of these options would make Go a far more dynamic language
I can't be sure what precisely "dynamic" means to a language expert, but I don't see the
second option making the language more or less dynamic than it is already?
You can already obtain both types and fields "by name", as follows:
    type MyType struct {
        MyField string
    }
    t := reflect.TypeOf((*MyType)(nil)).Elem() // 'MyType
    
    f, _ := t.FieldByName("MyField") // 'MyType.MyField
It "works" - it's just cumbersome and error-prone.
But in a nutshell, that is all I'm proposing - syntactic sugar for the same thing, and
some lightweight, built-in types to represent those.
I'm not asking for something that supports every type and every expression - in other
words, not asking for C# or LINQ, just for a type-safe way to reference what can only be
referenced as strings right now.
> you need to consider how they behave for every type and every expression
Fields really are the only problem - because structs are sets, and the current
facilities only provide (type safe) direct access to the root of those sets, not to
their elements; that is really the only shortcoming I'm trying to address.
This can all be statically checked and resolved at compile-time - I'm not asking you to
index all types in-memory by name, I realize that would change the dynamic of the
language entirely.

@ianlancetaylor
Copy link
Contributor

Comment 3:

> I'm not asking for something that supports every type and every expression
Understood, but Go is by design a language that uses a relatively small number of
orthogonal constructs.  We are not going to introduce a new language feature and say
"but you can only use it for structs and struct fields."

@gopherbot
Copy link
Author

Comment 4 by rasmus@mindplay.dk:

> We are not going to introduce a new language feature and say "but you can only use it
for structs and struct fields."
I guess I don't understand how else this feature would be useful or applicable to
anything else - you can already reflect on structs (and other types) with compile-time
checking, using e.g. reflect.TypeOf(MyType{}) ... so you don't need another way to solve
that problem.
To turn your argument around, why isn't that language feature already available for
fields the same as it is for structs? Or what's missing, or what went wrong, that the
existing feature don't "work" for fields?
In one sense, if MyType is a type of the kind struct, then MyType.MyField is a type of
the kind field; a field is a member of a set of fields in a type of kind struct, but it
is also it's own thing. Field values attach and separate from their parent structs
naturally and easily - but field "types" do not willing separate from their parent
struct and are unreachable without them.
I'm trying to view this from a conceptual point of view rather than from a technical
point of view - I realize the technical side of it is more complicated than that, but
conceptually, it seems somewhat incomplete when viewed that way... where by "complete",
I mean fields, not just their values, are a tangible thing that you can refer to, the
same way you refer to a type.
I guess, where else, or how, or in what way do you picture this feature would be
applicable? Or are you saying it's not, and therefore is not useful or general enough to
be justified? (if so, I guess I would argue it's not so much a new feature, and perhaps
in a sense more the completion of another existing feature...)

@ianlancetaylor
Copy link
Contributor

Comment 5:

> I guess I don't understand how else this feature would be useful or applicable to
anything else
All of your examples add new syntax.  There is no obvious reason why that syntax should
not work on types that are not structs.  To me the argument that the syntax is not
useful on other types is a reason to not adopt the syntax, since it is not orthogonal. 
But if we do adopt the syntax, it has to do something.  It would make no sense to me to
say that 'Foo is an error if Foo is not a struct type; what would be the basis for that?
> In one sense, if MyType is a type of the kind struct, then MyType.MyField is a type of
the kind field
OK, sure.  But the language supports naming types, and it does so for a good reason: you
can declare variables with the named type.  The language does not support naming fields
(in the sense that you mean).  That's because there isn't much you can do with a named
field.  For example, you can not declare a variable as having the type "field".
As far as I can tell, you have a very specific use case: you want to obtain a value of
type reflect.StructField from something other than the existing mechanisms.  In order to
do this you are proposing quite broad ranging modifications to the language, but you
aren't following through on your proposal, you are just saying "there is this new way to
name dynamic language constructs, it's built into the language instead of calling the
reflect package, but you can only use it for struct fields."
A struct field fundamentally requires two concepts: a struct and a field.  You could
write a function that does what you want today by taking two arguments, a struct and a
field (p := &S{}; f := Field(p, &p.F)).  Or you could do it using the existing language
feature unsafe.Offsetof (f := Field(S{}, unsafe.Offsetof(S{}.F)).  I agree that these
are not especially pretty, but if you want pretty it needs to be orthogonal and clean,
not just specific for struct fields.

@bradfitz bradfitz removed the new label Dec 18, 2014
@rsc rsc added this to the Unplanned milestone Apr 10, 2015
@rsc rsc changed the title language: static field (and type) references proposal: spec: static field (and type) references Jun 20, 2017
@rsc rsc added the v2 A language change or incompatible library change label Jun 20, 2017
@wrouesnel
Copy link

So I stumbled on this issue looking to see if something like this functionality existed (or had been proposed). The specific use case which prompted me to think about it is that it would be nice to be able to do something like the following:

import "time"

type StructWhichMaintainsState struct {
	IPAddress string
	Name string
	NumberOfThings int
	LastUpdate time.Time
}

// In this function definition, channel "ch" is defined as a field type on
// StructWhichMaintainsState. The enforcement here is that whatever you put into
// this channel must be cast to the same type as the struct field.
func ipUpdater(name string, ch chan StructWhichMaintainsState.IPAddress) {
	for {
		ipAddr := doSomethingWhichGetsTheIP(name)
		castedStringToFieldUpdate := StructWhichMaintainsState.IPAddress(ipAddr)
		ch <- castedStringToFieldUpdate
		time.Sleep(time.Second)
	}
}

func stateManager(state *StructWhichMaintainsState) {
	// Channel carrying any data type
	updateCh := chan interface{}

	// This cast should probably be optional? But its here for c
	go ipUpdater(state.Name, (chan StructWhichMaintainsState.IPAddress)updateCh)

	// The real power is here: the type switch here lets us select between
	// multiple incoming "field" types off the one channel - removing the need
	// for definiting separate struct types for communicating these types of
	// "patch" updates to a struct.
	for incomingUpdates := range updateCh {
		switch v := incomingUpdates.(type) {

		// We could do something like this...
		case StructWhichMaintainsState.IPAddress:
			state.IPAddress = v
		// But for real convenience we would instead like to just do this:
		case state = <- v:
			// This syntax says "apply the value of v to a matching field type
			// within the target struct. Since a field type matches *exactly*
			// that named field, this is a predictable shorthand.
		}

		state.LastUpdate = time.Now()
	}
}

func main() {
	// Some big map we need to lookup infrequently, which holds a lot of structs
	// of variable state.
	mapOfState := make(map[string]*StructWhichMaintainsState)

	// We make some item which will receive updates here and launch a goroutine
	// to keep updating it.
	stateMan := &StructWhichMaintainsState{Name: "name1"}
	go stateManager(stateMan)
	mapOfState["name1"] = stateMan

	stateMan := &StructWhichMaintainsState{Name: "name2"}
	go stateManager(stateMan)
	mapOfState["name2"] = stateMan
}

The syntax isn't dead on, but the concept is that a "named field type" allows going from an interface{} to updating the correct field. type switch syntax and casts can be used to convert back and forth at data ingest points.

@ianlancetaylor
Copy link
Contributor

We aren't going to be making this kind of change.

It might be possible to discuss some sort of typeof predefined function that somehow returns the type of its argument in a way that can be used elsewhere, so you could, for example, declare a variable that has the same type as some other variable or struct field. But that would be complicated, because it would permit things that can not happen today, such as defining a struct with fields whose types are those of some unexported type from some other package (if the type were available via an exported variable or field, for example).

Closing this issue.

@the1mills
Copy link

the1mills commented Dec 1, 2018

Yeah this sucks. For metaprogramming etc, we need to tag structs with meta-info. Static fields are perfect for that. Until then, I think you can store a struct in a map as the key, and put your static fields as the values in the map?

@golang golang locked and limited conversation to collaborators Dec 1, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

6 participants