Skip to content

Proposal: implicit Object instantiation for *js.Obect-special struct types #634

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
myitcv opened this issue Apr 25, 2017 · 15 comments
Open
Labels

Comments

@myitcv
Copy link
Member

myitcv commented Apr 25, 2017

Following discussion with @neelance and @shurcooL

This proposal is a follow on from #633, that is to say, please read this proposal as if #633 has been approved, implemented and merged.

Consider the following *js.Object-special struct type declaration:

type T struct {
	o *js.Object `jsspecial:"true"`  // tag name still TBC in #633

	Name     string `js:"name"`
	Age      int    
	location string
}

At present, as described in the wiki, when creating a value of type *T via a struct literal, you have to initialise the fields separately:

v0 := &T{o: js.Global.Get("Object").New()}
v0.Name = "Peter"
// ...

We propose that values of type *T can be created in either one of two ways:

  1. &T{o: someO}
  2. &T{Name: "Peter"}

The first case represents no change over the current situation.

In the second case, an Object is implicitly instantiated for us, so it's as if we'd written:

v := &T{Name: "Peter"}

// is equivalent to

v := &T{o: js.Global.Get("Object").New()}
v.Name = "Peter"

Note, by preventing a mixing of the two forms we remove any questions/ambiguity around order of evaluation etc. Mixing of the two forms would result in a compiler error:

v := &T{o: js.Global.Get("Object").New(), Name: "Peter"}  // ERROR

This proposal leans heavily on the discussion in #236 (thanks for raising @Archs and to @theclapp and others for the ensuing discussion)

The clearer semantics introduced in #633 make this proposal itself much clearer, but also allow for fluent, unambiguous use of composite literals for *js.Object-special struct types.

@flimzy
Copy link
Member

flimzy commented Apr 25, 2017

Note, by preventing a mixing of the two forms we remove any questions/ambiguity around order of evaluation etc.

Can you clarify what you meant by "mixing of the two forms"?

@myitcv
Copy link
Member Author

myitcv commented Apr 25, 2017

Can you clarify what you meant by "mixing of the two forms"?

Thanks for the prompt - I should have included such an example... modified the headline description accordingly.

@tj
Copy link

tj commented May 6, 2017

Ah perfect this is what I was looking for. Big +1 from me. My previous gopherjs tests had been primarily JS -> Go for bindings, however now that I'm writing some that are primarily Go -> JS with several dozens of structs, it's extremely painful to lose the ability to use the struct literal and assign each field on its own.

I think this is especially true since it ruins what would otherwise be a normal public API, now you have to interact with it differently just because of gopherjs. Unless there's a trick everyone is using? I could see having two versions of the same struct and assigning them manually etc, or converting to a js.M{} internal to the function, but that's a lot of work.

Happy to help implement this if it seems like something a gopherjs internals noob could dive into.

@theclapp
Copy link

theclapp commented May 7, 2017

Edit: Updated New() after some real usage.
Edit 2: New() doesn't work with more complex types, e.g. with []string fields.

Here's a sketch of an implementation of this idea in current GopherJS, if you don't mind the use of js.InternalObject() to get at the "outer" versions of a js-special field.

Given a

type T struct {
	*js.Object
	i int `js:"i"`
	j int
}

we're trying to automate something like this:

func NewT(t *T) *T {
	t.Object = js.Global.Get("Object").New()
	io := js.InternalObject(t)
	t.Set("i", io.Get("i"))
	return t
}

Example:

t := NewT(&T{i: 1, j: 2})
println("t:", t, "t.i:", t.i, "t.j:", t.j)
// => t: Object {$val: Object, Object: Object, i: 1, j: 2} t.i: 1 t.j: 2
	
t.i = 3
println("t:", t, "t.i:", t.i, "t.j:", t.j)
// => t: Object {$val: Object, Object: Object, i: 1, j: 2} t.i: 3 t.j: 2
// Note that the "outer" i hasn't changed      ^^^^

This function assumes that #633 has not been implemented, so there are the usual "inner" and "outer" parts of a js-special struct, and a field you want to go into the "inner" Object has to have a js tag, whereas a field without a js tag will be stored in the "outer" object.

// Edit: Warning: New() doesn't work with more complex types, e.g. []string fields.
// It's a neat toy, so far, but not production ready.  Test carefully.
func New(t interface{}) interface{} {
	io := js.InternalObject(t)
	valueOfT := reflect.ValueOf(t).Elem()

	// If the first field (assumed to be the *js.Object field) is set, just
	// return t unchanged.  Does no other error checking.
	f0Name := valueOfT.Type().Field(0).Name
	if io.Get(f0Name) != nil {
		return t
	}

	if !valueOfT.Field(0).CanSet() {
		// reflect's Set method won't set unexported fields
		panic("The *js.Object field must be exported")
	}

	typ := valueOfT.Type()
	obj := js.Global.Get("Object").New()

	for field := 1; field < typ.NumField(); field++ {
		if jsName, ok := typ.Field(field).Tag.Lookup("js"); ok {
			goName := typ.Field(field).Name
			obj.Set(jsName, io.Get(goName))
		}
	}

	valueOfT.Field(0).Set(reflect.ValueOf(obj))
	return t
}

Example:

t2 := New(&T{i: 10, j: 11}).(*T)
println("t2:", t2, "t2.i:", t2.i, "t2.j:", t2.j)
// => t2: Object {$val: Object, Object: Object, i: 10, j: 11} t2.i: 10 t2.j: 11

t2.i = 13
println("t2:", t2, "t2.i:", t2.i, "t2.j:", t2.j)
// => t2: Object {$val: Object, Object: Object, i: 10, j: 11} t2.i: 13 t2.j: 11
// "outer" i still hasn't changed.              ^^^^^

Playground link. Edit: This link does not have the updated New() code listed above, though it is similar.

I've only just written this today and haven't played with it much beyond the examples given, but it seems promising.

This function doesn't implement some of the error checking specified. In particular it ignores this part "Mixing of the two forms would result in a compiler error". Also, since it's written using the stock reflect package, the *js.Object field must be exported (though it can be embedded). reflect.Set() doesn't let you set unexported fields.

@myitcv
Copy link
Member Author

myitcv commented May 8, 2017

@tj

now that I'm writing some that are primarily Go -> JS with several dozens of structs, it's extremely painful to lose the ability to use the struct literal and assign each field on its own....

I think this is especially true since it ruins what would otherwise be a normal public API, now you have to interact with it differently just because of gopherjs.

This was exactly my motivation behind raising this for discussion so 👍 to your comments.

Unless there's a trick everyone is using? I could see having two versions of the same struct and assigning them manually etc, or converting to a js.M{} internal to the function, but that's a lot of work.

I'm working around this for now in myitcv.io/react using an approach not dissimilar to that which you describe. Here's an example which is paraphrased below:

type _PProps struct {
	o *js.Object

	ClassName string `js:"className"`
}

type PProps struct {
	ClassName               string
}

func (p *PProps) assign(v *_PProps) {
	v.ClassName = p.ClassName
}

func P(props *PProps, children ...Element) *PDef {
	rProps := &_PProps{
		BasicHTMLElement: newBasicHTMLElement(),
	}

	if props != nil {
		props.assign(rProps)
	}

	// ...
}

So as a user of the <p> element, I use the interface of PProps which is a regular Go struct but under the covers this gets translated into a _PProps "instance" (which is just a regular Javascript object).

However in my case this proxying is code generated so I can sympathise with a situation where this isn't possible/practical...

Side note

To build on your point about a "normal public API" there is another thing to note with the props example. Only making mention of this to clarify what's going on in the example above.

I'm effectively using the _*Props structs as templates, for example _PProps:

type _PProps struct {
	*BasicHTMLElement
}

gets "exploded" into PProps:

type PProps struct {
	ClassName               string
	DangerouslySetInnerHTML *DangerousInnerHTMLDef
	ID                      string
	Key                     string

	OnChange
	OnClick

	Role  string
	Style *CSS
}

in order that the caller can use composite literals without needing to explicitly reference embedded struct types.

With the explosion we can do:

p := &PProps{
	ClassName: "example",
}

without it we'd be left to do:

p := &PProps{
	BasicHTMLElement: &r.BasicHTMLElement{
		ClassName: "example",
	},
}

@myitcv
Copy link
Member Author

myitcv commented May 8, 2017

@theclapp - not quite sure I follow. Are you suggesting we should retain the inner and outer parts of *js.Object-special structs?

@theclapp
Copy link

theclapp commented May 8, 2017

@myitcv No, it was mostly because I think this proposal would be great, and have wanted it or something like it myself for a long time, so I implemented it and posted it here. If this proposal is never implemented, or takes a long time, in the meantime we have some code that works now that gets us 80% of the way there.

It might also conceivably provide a guide to whoever eventually implements this proposal.

[Comment about outer & inner moved to #633, here]

@myitcv
Copy link
Member Author

myitcv commented May 9, 2017

@theclapp great, sounds like we're all arriving at the same solution in some way shape or form!

If this proposal is never implemented, or takes a long time, in the meantime we have some code that works now that gets us 80% of the way there.

#617, #633 and #644 are the next things I'm going to work on, in that order. #633 will require coordination with authors of bindings (because it's a somewhat of a breaking change). But that said, all three issues are effectively on my critical path so selfishly at least I want to see them solved 👍

@theclapp
Copy link

Under this proposal, you could still say t := &T{}, right? And t.Object would still == nil?

@myitcv
Copy link
Member Author

myitcv commented May 15, 2017

@theclapp - (good question because I think this particular case could do with an explicit mention)

The underlying *js.Object field would be initialised unless it was supplied so:

v := &T{}

// would be equivalent to

v := &T{o: js.Global.Get("Object").New()}

Can you see any issues with this?

@theclapp
Copy link

I don't think so. I thought I would when I asked the question in the first place, but on further consideration I think not.

It helps me to imagine that we've redefined the zero-value for a js-special struct. In the same way that you can immediately assign to fields in a regular Go struct after creating it, you can also under this proposal immediately assign to fields in a js-special struct after you've created it.

type T1_Go struct {
  i int
}

type T2_JS struct {
  o *js.Object `jsspecial:"true"`
  i int
}

t1 := &T1_Go{}
t1.i = 1
t2 := &T2_JS{}
t2.i = 1 // no muss, no fuss, same as a Go struct;
         // e.g. no need to initialize t2.o first.

@dmitshur
Copy link
Member

I can't quite tell from this thread, but does the above assume a restriction (compilation error) is in place that prevents T2_JS values, only allowing pointers to it (i.e., *T2_JS values)?

@theclapp
Copy link

@shurcooL I don't think we assume that, though as I understand it, and from what I've seen, non-pointer T2_JS would be problematic at present. That's also what the Wiki says:

A struct that embeds a *js.Object must always be a pointer.

type t struct {
	*js.Object // so far so good
	i int `js:"i"`
}

var v = t{}   // wrong: not a pointer
var v2 = &t{} // right

(Of course, pretty sure I wrote that part of the wiki!)

... And yet playing around with it briefly in the playground seems to work. Has that always worked and I was just mistaken, or has something changed?

@myitcv
Copy link
Member Author

myitcv commented May 15, 2017

As @theclapp says, it's certainly not set in stone according to how things are currently written:

We could even make it a compiler error to use a non-pointer value of such types (a point which is currently only enforced via guidance in the wiki)

Theoretically (subject somewhat to the current discussion in #633) there is no reason why non-pointer values would not work.

But to my mind by enforcing the use of pointer types (and values) we better reflect the fact that the underlying *js.Object has pointer semantics (there is no struct value equivalent in Javascript). This was in fact my understanding of why this rule existed in the first place (notwithstanding any reasons that might exist because of the existence of an "outer" part in the current implementation).

So to agree with a point that @flimzy made earlier (where he even suggested we implement this now, before these 3 proposals) we should make it a compile time error when the non-pointer type is used.

@dmitshur
Copy link
Member

Ok. Because, yeah, if values are allowed, it would mean the zero values would not be equal, which would be weird:

var t1, t2 T1_Go
fmt.Println(v1 == t2) // true
var t3, t4 T2_JS
fmt.Println(t3 == t4) // false (because t3.Object != t4.Object)

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

No branches or pull requests

5 participants