Skip to content

Proposal: explicit *js.Object-special struct types #633

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 24, 2017 · 27 comments
Open

Proposal: explicit *js.Object-special struct types #633

myitcv opened this issue Apr 24, 2017 · 27 comments
Labels

Comments

@myitcv
Copy link
Member

myitcv commented Apr 24, 2017

Following discussion with @neelance and @shurcooL

Currently (as of 265040a to be explicit) *js.Object-special struct types are inferred from struct types which have a *js.Object-typed field as the first field and a least one field with a js tag (as described in the wiki):

type T struct {
	o *js.Object

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

We propose changing this to a more explicit approach where the *js.Object-typed field is tagged with jsspecial:"true", and the js field tags are optional:

type T struct {
	o *js.Object `jsspecial:"true"`  // let's bikeshed the tag name...

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

Under this proposal it would be an error for a struct to declare more than one field annotated with jsspecial:"true" (after embedding).

The impact of this change would be:

  • to make *js.Object-special struct types more explicit and therefore easier to explain/understand. 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)
  • to do away with the "inner" and "outer" parts (using the wiki parlance), leaving just the "inner" part (which we can then refer to as effectively a *js.Object value). This helps remove any sort of confusion around there being two parts: every assignment to a field is redirected onto the *js.Object value in the jsspecial tagged field
  • by making the js tag optional for fields we follow a pattern much closer to, for example, encoding/json tags, i.e. a familiar pattern (with the exception that even unexported fields are supported). The js tag would then only be required where a different name is required on the underlying *js.Object

Here's a quick example under this proposal:

package main

import (
	"github.com/gopherjs/gopherjs/js"
)

type T struct {
	o *js.Object `jsspecial:"true"`

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

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

	o.Name = "Lorem"
	o.Age = 100
	o.location = "Ipsum"

	js.Global.Get("console").Call("log", o)

	// outputs
	//
	// { name: 'Lorem', Age: 100, location: 'Ipsum' }
}

to be clear, under the current implementation this outputs simply { name: 'Lorem' }

The following would however be a compile error for struct type S:

type T struct {
	o *js.Object `jsspecial:"true"`

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

type S struct {
	*T

	p *js.Object `jsspecial:"true"`  // ERROR multiple fields tagged `jsspecial:"true"` in S
}

This would be a "breaking" change:

  1. developers will need to annotate their *js.Object fields with the new tag
  2. anyone relying on the two parts of such objects will lose the "outer" part

The first of these points is easily fixed; making the change also allows the new code to be backwards compatible with previous versions of GopherJS.

It's assumed that very, very few people (indeed anyone?) are relying on the "outer" part in the current implementation; please shout if that's a bad assumption

Related:

Open questions

#### Q1: Do we make methods defined on *js.Object-special struct types an error?

Context

Probably, yes.

Q2: What should the identifying tag for such struct types be?

  1. jsspecial:"true" - ruling this out because it was intentionally provocatively bad
  2. mapjsfields:"true" @neelance
  3. js:"*" @flimzy

Q3: Add migration plan to the proposal for feedback

That. This proposal is recognised as a "breaking" change... so we need to make clear what that means and how the migration path will minimise impact, the process by which breakages are fixed etc.

@neelance
Copy link
Member

👍
I agree that this would make the feature more explicit and easier to use.

My contribution to the bikeshedding: What about mapjsfields:"true"?

@4ydx
Copy link
Contributor

4ydx commented Apr 25, 2017

Under the new proposal would the following be compliant:

type A struct {
	*js.Object `mapjsfields:"true"`

	Values []*B
}

type B struct {
	*js.Object `mapjsfields:"true"`

	Name string
        Email string
}

@flimzy
Copy link
Member

flimzy commented Apr 25, 2017

I think I like this proposal. My specific comments below.

We could even make it a compiler error to use a non-pointer value of such types

This sounds like a big improvement, that wouldn't even need to wait for this proposal. Is there a reason (other than being a breaking change) this behavior couldn't be implemented immediately?

Under this proposal it would be an error for a struct to declare more than one field annotated with jsspecial:"true" (after embedding).

I wonder if this tag is really even necessary. Go already prohibits duplicate fields, so there will only ever be one embedded *js.Object (unless I'm overlooking something?).

Maybe the thought is that someone might want to disable this functionality by not specifying the tag? Is that a meaningful use-case?

If the tag is deemed necessary, then my bikeshedding contribution would be to use the existing js tag, with a special value. Perhaps js:"*".

@flimzy
Copy link
Member

flimzy commented Apr 25, 2017

Another point for consideration: It might be desirable to disable this functionality on a per-field basis. Supporting js:"-" would be the natural way to express this. Although I expect this would once again give us the "internal" vs "external" object paradigm, even if only when explicitly requested.

The use case would be when wrapping a JS object that you don't want to pollute with Go-specific fields:

type Widget struct {
    *js.Object
    X int
    Y int
    mu sync.RWMutext `js:"-"` // A contrived example; could be any data type
}

Of course a work-around would be two structs in Go, effectively managing the "external" object explicitly.

type Widget struct {
    *js.Object
    X int
    Y int
}

type WidgetWrapper struct {
    *Widget
    mu sync.RWMutext `js:"-"`
}

@myitcv
Copy link
Member Author

myitcv commented Apr 25, 2017

Edit: strikethrough question to @neelance and @shurcooL

@4ydx I notice you updated your example to make the slice of type []*B 👍 - yes it would indeed be supported.

@flimzy - thanks for the feedback.

This sounds like a big improvement, that wouldn't even need to wait for this proposal. Is there a reason (other than being a breaking change) this behavior couldn't be implemented immediately?

Potentially, yes. I'm going to be focussing on this proposal in the first instance so if you or indeed anyone else would like to take up the mantle on making a more immediate change...

There is one related edge case we forgot @neelance @shurcooL - methods defined on such types will in effect be useless because they will be inaccessible. Presumably we make the declaration of methods (pointer receiver or otherwise) on such types a compiler error? I'll add an open question to the headline description on this.

I wonder if this tag is really even necessary. Go already prohibits duplicate fields, so there will only ever be one embedded *js.Object (unless I'm overlooking something?).

The *js.Object-typed field does not actually need to be anonymous/embedded; so yes, I think we need to be explicit. Indeed I intentionally made my example use a non-embedded field to try and make this clear:

type T struct {
	o *js.Object `jsspecial:"true"` 

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

Maybe the thought is that someone might want to disable this functionality by not specifying the tag? Is that a meaningful use-case?

Not that I can think of, but again I might be missing something. *js.Object-special struct types are, to all intents and purposes, a syntactic sugar around an actual *js.Object value.

If the tag is deemed necessary, then my bikeshedding contribution would be to use the existing js tag, with a special value. Perhaps js:"*".

I'll add that to the list (in the headline description)

Another point for consideration: It might be desirable to disable this functionality on a per-field basis. Supporting js:"-" would be the natural way to express this. Although I expect this would once again give us the "internal" vs "external" object paradigm, even if only when explicitly requested.

Related to my point above, I don't think it makes sense to disable this functionality. This is essentially where the analogy with encoding/json tags breaks down, because as I said this type of construct is purely syntactic sugar.

Of course a work-around would be two structs in Go, effectively managing the "external" object explicitly.

👍

@flimzy
Copy link
Member

flimzy commented Apr 25, 2017

The *js.Object-typed field does not actually need to be anonymous/embedded; so yes, I think we need to be explicit.

Hmm. I guess I missed that part of the proposal.

I was about to ask whether this was really appropriate, but I think in writing my question, I've convinced myself that it is. :) (Which I consider slightly unfortunate... I was hoping for a cleaner spec, but I digress...)

There is one related edge case we forgot @neelance @shurcooL - methods defined on such types will in effect be useless because they will be inaccessible. Presumably we make the declaration of methods (pointer receiver or otherwise) on such types a compiler error? I'll add an open question to the headline description on this.

You're talking about Go methods, right (not JS functions as object members). I'm not seeing immediately why this is problematic. Maybe you can elaborate a bit?

@myitcv
Copy link
Member Author

myitcv commented Apr 25, 2017

Hmm. I guess I missed that part of the proposal.

You didn't at all because it's possible today!

https://gopherjs.github.io/playground/#/25H-0WPIk7

I was about to ask whether this was really appropriate, but I think in writing my question, I've convinced myself that it is. :) (Which I consider slightly unfortunate... I was hoping for a cleaner spec, but I digress...)

Not sure I follow (the bit about a "cleaner spec")?

You're talking about Go methods, right (not JS functions as object members). I'm not seeing immediately why this is problematic. Maybe you can elaborate a bit?

Thanks for prompting me to actually think about this 👍

I've arrived at the same conclusion: it's not a problem.

@flimzy
Copy link
Member

flimzy commented Apr 25, 2017

Not sure I follow (the bit about a "cleaner spec")?

Just saying that not requiring an extra tag would be a bit cleaner. But that's irrelevant, since it's not a good option :)

@theclapp
Copy link

theclapp commented May 9, 2017

[First paragraph moved from my comment on #634 and edited slightly.]

I kind of like the inner and outer parts of js-special structs -- I kind of like the ability to have js-data and a channel or mutex in the same struct -- but I'm happy with the workaround of using a regular Go struct for the Go-bits and containing a js-special struct for the JS bits.

I like js:"*". This seems nicely parallel.

Would there still be outer and inner objects? That is, would the js fields be implemented as foo.Object.field_name? Or would there be only one object? E.g. if I have

type T struct {
	*js.Object `jsspecial:"true"` 

	Name     string `js:"name"`
	Age      int    
	location string
}
t := &T{Name: "Larry"}
println(t)

what would I see in the console? Would I see (something similar to) {Object: {name: "Larry"}} or just {name: "Larry"}?

If there are still outer and inner objects, I like @flimzy's idea of js:"-" to mark a Go-only field, that's in the "outer" object, which would allow Go values to share a structure with other js values.

@myitcv
Copy link
Member Author

myitcv commented May 9, 2017

Thanks for your feedback @theclapp

but I'm happy with the workaround of using a regular Go struct for the Go-bits and containing a js-special struct for the JS bits.

I think this is the direction we've pretty much headed in so good to hear that you can't think of any critical reasons against it.

Would there still be outer and inner objects? That is, would the js fields be implemented as foo.Object.field_name? Or would there be only one object?

I'll answer from the perspective of the Javascript VM (which is ultimately what you see in the debugger and all that matters at the end of the day)

Let's take your example (assuming we haven't implemented #634 for now):

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

This will transpile to:

t = new T.ptr(new ($global.Object)(), "");

So in Javascript VM terms there are two objects that have been instantiated here.

But under this proposal, setting any fields on t (in the Go world) will simply transpile to setting fields on the "inner" of those two objects:

t.Name = "Paul"
t.Age = 99

will transpile to:

t1.Object.name = $externalize("Larry", $String);
t1.Object.Age = 99

That is, under this proposal it will be impossible to declare and therefore set any fields on the "outer" object. Hence the js:"-" syntax does make any sense: you can't mark something as Go-only. Instead you use the workaround you describe.

So to answer your question precisely there are two objects at play. But from a Go perspective it's as if you're setting fields on just one (hopefully that makes sense?)

what would I see in the console?

That's a slightly different question because $externalize comes into play. Again, using the long-hand (pre #634) for now:

t := &T{Object: js.Global.Get("Object").New()}
t.Name = "Larry"
print(t)

will transpile to:

t = new T.ptr(new ($global.Object)(), "");
t.Object.name = $externalize("Larry", $String);
console.log(t);

Because we can now accurately identify *js.Object-special struct types, the $externalize-ing of a *T value is simply the value of the jsspecial:"true" field.

If you then want to bring a *js.Object value back into the Go world you will still need to do:

var v *js.Object

// v gets set somehow

t := &T{Object: v}

@flimzy
Copy link
Member

flimzy commented May 10, 2017

Just as an aside, before any breaking changes are introduced, it might be nice to get vendored GopherJS working so people who can't immediately update their code have some recourse. I think #415 is the issue tracking that.

@theclapp
Copy link

it might be nice to get vendored GopherJS working

In #415 it's unclear (to me at least) whether they (the maintainers) want vendoring to be a supported option, and it seems like 415 could be harder and/or more intricate than this issue and its relatives (#634).

So maybe just make a branch or tag or release or fork, so it's easy to get the "before #633" code, and leave it at that. (Just my two cents. I don't have any GopherJS in production, so it's easy for me to say "go ahead and break the world, I won't mind". :)

@dmitshur
Copy link
Member

before any breaking changes are introduced

We'll definitely need to figure out a viable transition plan. I think that should be described as part of the proposal.

In #415 it's unclear (to me at least) whether they (the maintainers) want vendoring to be a supported option

I've applied labels to make it more clear. It's a bug, it requires figuring out what's wrong and coming up with a fix. I'm not aware of anyone working on it right now.

@myitcv
Copy link
Member Author

myitcv commented May 11, 2017

@shurcooL

We'll definitely need to figure out a viable transition plan. I think that should be described as part of the proposal.

Good point and noted

@dominikh
Copy link
Member

I'd like to point out that js/dom currently contains structures like these:

type BasicNode struct {
        *js.Object
}

type BasicElement struct {
        *BasicNode
}

type BasicHTMLElement struct {
        *BasicElement
}

type URLUtils struct {
        *js.Object

        Href     string `js:"href"`
        // other fields
}

type HTMLAnchorElement struct {
        *BasicHTMLElement
        *URLUtils
        HrefLang string `js:"hreflang"`
        // other fields
}

where both BasicNode and URLUtils contain the same JavaScript object in their *js.Object. BasicHTMLElement and URLUtils both define fields and methods that make use of the embedded *js.Object. Under current versions of GopherJS, this is working fine. Will your proposed new requirements break this?

@myitcv
Copy link
Member Author

myitcv commented May 11, 2017

@dominikh thanks for the feedback; yours was feedback I was keen to solicit, particular when it comes to migration (but that can wait for later).

Rewriting part of your example in terms of this proposal:

type BasicNode struct {
        *js.Object `jsspecial:"true"` 
}

type BasicElement struct {
        *BasicNode
}

type BasicHTMLElement struct {
        *BasicElement
}

type URLUtils struct {
        *js.Object `jsspecial:"true"` 

        Href     string `js:"href"`
        // ...
}

the embedding of a struct with a jsspecial:"true" tag would work as expected yes.

But just to reiterate an earlier point, we lose the Go "side"... every field in a struct is the Javascript object.

...which leads nicely onto HTMLAnchorElement because left untouched the semantics would change.

Currently (i.e. pre this proposal):

var o = js.Global.Get("Object").New()
a := &HTMLAnchorElement{
	URLUtils: &URLUtils{
		Object: o,
	},
	BasicHTMLElement: &BasicHTMLElement{
		BasicElement: &BasicElement{
			BasicNode: &BasicNode{
				Object: o,
			},
		},
	},
}

a.HrefLang = "HrefLang"
a.Href = "href"

transpiles to:

o = new ($global.Object)();
a = new HTMLAnchorElement.ptr(new BasicHTMLElement.ptr(new BasicElement.ptr(new BasicNode.ptr(o))), new URLUtils.ptr(o, ""), "");

a.BasicHTMLElement.BasicElement.BasicNode.Object.hreflang = $externalize("HrefLang", $String);
a.URLUtils.Object.href = $externalize("href", $String);

which ends up modifying the same underlying object, o. This happens by virtue of the *URLUtils field being part of the Go "side" (because it doesn't have a js tag).

Under this proposal, the transpiled result looks quite different because the js tag is optional:

o = new ($global.Object)();
a = new HTMLAnchorElement.ptr(new BasicHTMLElement.ptr(new BasicElement.ptr(new BasicNode.ptr(o))), new URLUtils.ptr(o, ""), "");

a.BasicHTMLElement.BasicElement.BasicNode.Object.hreflang = $externalize("HrefLang", $String);
a.BasicHTMLElement.BasicElement.BasicNode.Object.URLUtils.href = $externalize("href", $String);

The *URLUtils field is considered to be a field by the same name on the underlying *js.Object (the HTMLAnchorElement type being considered a *js.Object-special struct type by virtue of the embedded *BasicHTMLElement field itself being a *js.Object-special struct type etc).

This is a breaking change therefore. And I think this snippet (taken from your honnef.co/go/js/dom package):

func wrapHTMLElement(o *js.Object) HTMLElement {
	if o == nil || o == js.Undefined {
		return nil
	}
	el := &BasicHTMLElement{&BasicElement{&BasicNode{o}}}
	c := elementConstructor(o)
	switch c {
	case js.Global.Get("HTMLAnchorElement"):
		return &HTMLAnchorElement{BasicHTMLElement: el, URLUtils: &URLUtils{Object: o}}

	// ...

confirms as much (hope you agree?).

However, I wonder whether there is an alternative, picking up on this comment...

But before I go down that path too much, let me first clarify: is the reason you're embedding in this way to effectively "subclass"? i.e. re-use the definition of URLUtils in multiple other types?

@theclapp
Copy link

Under this proposal, the transpiled result looks quite different because the js tag is optional

a.BasicHTMLElement.BasicElement.BasicNode.Object.hreflang = $externalize("HrefLang", $String);
a.BasicHTMLElement.BasicElement.BasicNode.Object.URLUtils.href = $externalize("href", $String);

Do you have a working fork that generates that, or are you more or less forecasting? 'Cause that seems odd.

It's kind of weird that HTMLAnchorElement doesn't have a jsspecial:"true" tag anywhere (except 3 and 1 definition deeper), and yet the definition of BasicHTMLElement changes how URLUtils and HrefLang are interpreted.

If I understand correctly, this breaks the idea of embedding a JS-special struct in a Go struct with other regular Go fields. If JS-special-ness is "transitive upwards", so to speak, then any field in a struct with a js-special struct deeper in its definition becomes a js field. E.g., S3 won't work:

type S1 struct {
	*js.Object
	I int `js:"i"`
}
type S2 struct {
	ch chan int
}
type S3 struct {
	*S1
	S2
	ch2 chan int
	J   int `js:"j"`
}
type S4 struct {
	S2
	*S1
}

Again, IIUC, in S3 you'd have S2 (and thus S2.ch) and ch2 be fields in S1.Object, which won't work (channels aren't externalizable). And S2 isn't defined as a pointer, which in a regular Go struct would be fine, but in this case, I don't know. And then what if you change the order of S1 and S2 (like in S4), what (if anything) happens there? Is the "transitive-upwardness" of js-special-ness conditional on the js-special field being the first field? That starts to get weird.

(Playing with these ideas here.)

Sorry, I'm confused.

Bottom line: how how would @dominikh achieve what he has now under this proposal?

@dominikh
Copy link
Member

@myitcv In terms of the DOM API, both URLUtils and HTMLAnchorElement are interfaces, and HTMLAnchorElement uses URLUtils as a mixin.

On the Go side, DOM interfaces translate to structs, and mixins are ordinary composition (or what you'd call "subclassing").

@myitcv
Copy link
Member Author

myitcv commented May 12, 2017

@dominikh thanks.

In terms of the DOM API, both URLUtils and HTMLAnchorElement are interfaces, and HTMLAnchorElement uses URLUtils as a mixin.

Indeed, in https://www.w3.org/TR/html5/ terms.

On the Go side, DOM interfaces translate to structs, and mixins are ordinary composition (or what you'd call "subclassing").

Yes I couldn't quite reconcile the use of "subclass" myself... mixin is better. But the key is that when this code gets transpiled there is a single underlying Javascript object, as you say. Either which way...

What about if we code-generated the types in dom, such that the re-use happens within the code generator as opposed to via composition?

With code generation, consider the resulting HTMLAnchorElement type for example:

type HTMLAnchorElement struct {
	*js.Object `jsspecial:"true"`

	// interface HTMLAnchorElement
	//
	Target   string `js:"target"`
	Download string `js:"download"`
	HrefLang string `js:"hreflang"`

	// ...

	// interface HTMLElement
	//
	Title string `js:"title"`
	Lang  string `js:"lang"`

	// ...

	// interface URLUtils
	//
	Href     string `js:"href"`
	Protocol string `js:"protocol"`

	// ...
}

Then instantiation is easy:

a := &HTMLAnchorElement{Object: js.Global.Get("Object").New()}
a.Lang = "Martian"

(and indeed with #634 we could go one better to a := &HTMLAnchorElement{Lang: "Martian"})

For edge cases we'd still have the option of writing methods etc. Just looking to leverage some code generation to avoid the repetitive parts where there is clear reuse.

Does this also perhaps help to present a better "interface" to the user, per @tj's comment in the linked thread?

I ask because composition doesn't map well to what these *js.Object-special struct types get transpiled to... hence why we currently have this sort of thing when creating values:

a := &HTMLAnchorElement{
	URLUtils: &URLUtils{
		Object: o,
	},
	BasicHTMLElement: &BasicHTMLElement{
		BasicElement: &BasicElement{
			BasicNode: &BasicNode{
				Object: o,
			},
		},
	},
}

I'm already doing something similar with myitcv.io/react... so if you're open to this idea then we can look to do something similar for honnef.co/go/js/dom

@dominikh
Copy link
Member

To be frank, I'm not really interested in making js/dom more complex (code generation), while also breaking backwards compatibility and reducing features. Right now, you can access the embedded URLUtils and pass it around as its own thing, which is a decent compromise considering we can't use Go interfaces to match struct fields.

@myitcv
Copy link
Member Author

myitcv commented May 12, 2017

breaking backwards compatibility

This is definitely what we need to consider carefully.

Are you of the opinion therefore that the costs outweigh the benefits for this proposal (from a honnef.co/go/js/dom perspective)?

reducing features

Aside from the "feature" of embedded URLUtils, is there anything else we lose?

Right now, you can access the embedded URLUtils and pass it around as its own thing, which is a decent compromise considering we can't use Go interfaces to match struct fields.

Where does this "feature" get used?

That said, I'm not sure that I would agree this is a "feature", more an unintended side effect. It comes at the cost of the bizarre creation construct I included in my previous comment. In any case, if someone wants to achieve the same effect they can re-use the underlying *js.Object for a different type, which is in effect what's happening at the moment in any case.

@dominikh
Copy link
Member

Are you of the opinion therefore that the costs outweigh the benefits for this proposal (from a honnef.co/go/js/dom perspective)?

I can't give a proper cost assessment yet.

Where does this "feature" get used?

Well, it's part of the public API of js/dom, so any of its users may make use of that fact. In particular, any library or helper that operates on any URL-like things would benefit from accepting a URLUtils and instantly work on all link-like objects in the DOM.

That said, I'm not sure that I would agree this is a "feature", more an unintended side effect

I would very much call it a feature. It's composition like it is typical for Go. A thing that consists of other things.

It comes at the cost of the bizarre creation construct I included in my previous comment

Other than having to specify the same *js.Object twice, I don't see how this is any more bizarre than any other embedded struct in ordinary Go.

More importantly, if we move past my specific case, we arrive at #633 (comment) and not being able to intuitively embed structs as soon as any of these structs is backed by a *js.Object. While my example uses the same *js.Object for both structs, it's easy to think of cases where they might be different objects, composed into a larger type that makes use of the embedded types. The embedding type might not even want to store any of its own fields in any of the embedded *js.Objects.

@theclapp
Copy link

theclapp commented May 12, 2017

@myitcv said:

But the key is that when this code gets transpiled there is a single underlying Javascript object

Not sure I agree. There's a single underlying JS object because the structs were initialized that way:

a := &HTMLAnchorElement{
	URLUtils: &URLUtils{
		Object: o,
	},
	BasicHTMLElement: &BasicHTMLElement{
		BasicElement: &BasicElement{
			BasicNode: &BasicNode{
				Object: o,
			},
		},
	},
}

Given a different use-case there could just as easily be distinct Object slots in URLUtils and BasicNode. My point being, you seem to be saying that that's an inherent property of the compiled code and I don't think that's the case.

The core of this problem seems to be the assumption that js-special-ness travels upwards. How can we fix that?

Currently, if you have a js-special object as your first field, either directly or embedded however deeply, then you can access that object by adding a js tag to any field. If I recall correctly (and I might not), the compiler sees the js tag and then looks for the *js.Object in field 0, and if it can't find one it throws an error.

The key there is that the field "asks" for special treatment, rather than some other part of the struct (field 0) "imposing" special treatment at a distance.

Could we require that if you embed a js-special struct and want its special-ness to include the current struct (and thus want the current struct to be a "pure Javascript" object), then you have to tag the embedded field, too?

Example:

type BasicNode struct {
        *js.Object `jsspecial:"true"`
}

type BasicElement struct {
        *BasicNode `jsspecial:"true"`
}

type BasicHTMLElement struct {
        *BasicElement `jsspecial:"true"`
}

type URLUtils struct {
        *js.Object `jsspecial:"true"`

        Href     string `js:"href"`
        // other fields
}

type HTMLAnchorElement struct {
        *BasicHTMLElement `jsspecial:"true"`
        *URLUtils `js:"-"`
        HrefLang string `js:"hreflang"`
        // other fields
}

This requires the js:"-" tag that's been discussed. We don't want URLUtils to be a field on BasicHTMLElement.Object.

Alternatively, without the js:"-" tag:

type HTMLAnchorElement_Inner struct {
        *BasicHTMLElement `jsspecial:"true"`
        HrefLang string `js:"hreflang"`
        // other js-only fields
}

type HTMLAnchorElement struct {
        *HTMLAnchorElement_Inner
        *URLUtils
        // other Go-only fields
}

It makes initialization somewhat more complex:

a := &HTMLAnchorElement{
	URLUtils: &URLUtils{
		Object: o,
	},
	HTMLAnchorElement_Inner: &HTMLAnchorElement_Inner{
		BasicHTMLElement: &BasicHTMLElement{
			BasicElement: &BasicElement{
				BasicNode: &BasicNode{
					Object: o,
				},
			},
		},
	},
}

but you could also say this, I think:

a := &HTMLAnchorElement{}
a.HTMLAnchorElement_Inner.Object = o
a.URLUtils.Object = o

(although you could also do that now).

Without the js:"-" tag this does hint at some annoyance with us blithely saying "oh just wrap both js-special objects in a Go object". In the above code, that's kind of awkward, if you ask me.

Edit: Typo.

@myitcv
Copy link
Member Author

myitcv commented May 12, 2017

@theclapp @dominikh

I need to address the point that @theclapp made earlier (and @dominikh referenced) that starts:

It's kind of weird that HTMLAnchorElement doesn't have a jsspecial:"true" tag anywhere (except 3 and 1 definition deeper), and yet the definition of BasicHTMLElement changes how URLUtils and HrefLang are interpreted...

There is an issue here to discuss, you're right... will respond when I get a chance over the weekend.

@myitcv
Copy link
Member Author

myitcv commented May 15, 2017

@dominikh @theclapp - thanks for the various responses.

By way of a preamble, the motivation behind this proposal was to make the definition of *js.Object-special struct types simpler and therefore easier to use/understand. The goal was to reach a point where a value of a pointer to such a type could, to all intents and purposes, be considered (a wrapper around) a regular Javascript object, with the fields on such struct types being syntactic sugar to that object's properties. Hence why I've been referring to "a single underlying object", but also why the proposal seeks to do away with the "inner" and "outer" parts (because when such a value gets $externalize-ed the "outer" part is discarded in any case).

I completely concede, @theclapp, that given the current implementation it's entirely possible to have n underlying objects... that's partly the confusion I'm looking to avoid

But just to emphasise this is my intention alone... yes it was the context for this proposal but it's certainly not set in stone. Indeed the whole point of putting together this proposal was to flush out problems, like the gap you have found in the specification.

So picking up this problem, namely not being able to intuitively embed structs as soon as any of these structs is backed by a *js.Object. Completely agree this is an issue, and furthermore the solution you propose @theclapp seems entirely sensible, i.e. explicitly annotating a single embedded field with jsspecial:"true":

type BasicNode struct {
        *js.Object `jsspecial:"true"`
}

type BasicElement struct {
        *BasicNode `jsspecial:"true"`

        // ...
}

It enables us to unambiguously specify/distinguish between the following two types:

type T1 struct {
        *BasicElement `jsspecial:"true"`

        Banana string
}

type T2 struct {
        *BasicElement

        Apple string
}
  • T1 is a wrapper around a Javascript object; it is a *js.Object-special struct type (BasicElement is also therefore a *js.Object-special struct type). All values must be of type *T1
  • T2 is a regular Go struct type that embeds a *js.Object-special struct type.

@dominikh @theclapp I think this deals with the ambiguity - please shout if you disagree.

So back to @dominikh's HTMLAnchorElement example. The main issue brought up by this case is (I think, and please say if you think it's a different issue) whether embedding multiple *js.Object-special struct types is the best way of achieving code reuse/mixins.

Here is the current (i.e. pre-proposal) implementation annotated with the proposed tags:

type HTMLAnchorElement struct {
        *BasicHTMLElement    `jsspecial:"true"`
        *URLUtils            `jsspecial:"true"`

        HrefLang string `js:"hreflang"`
        // other fields
}

@dominikh I've definitely come round to the idea that embedding such types means that you're extending/mixing-in/whatever the containing type (if we gloss over the fact that name conflicts in Go and Javascript worlds are handled differently). In the example above I've annotated two such fields with the special tag.

Indeed we could even provide a convenience function called Wrap in the js package to handle the setting of the n (where n = 2 in this example) underlying objects:

package js

// Wrap takes o, a *js.Object Javascript object and a non-nil pointer i
// to a *js.Object-special struct type and sets the underlying *js.Object
// of i to o
//
func Wrap(o *js.Object, i interface{})

But the need for such constructors (or indeed the Wrap function) sits a little uneasy... I think it boils down to the fact that fundamentally there is a single underlying *js.Object (given that's what we're trying to wrap). Yet in our Go representation of it, the various parts (the embedded struct types) have their own underlying *js.Object values which we need to coordinate to be the same value... the fact that one of those parts can be nil or some other *js.Object feels rather brittle.

In an attempt to flesh out an alternative I put together the following, where essentially the code re-use (of BasicHTMLElement, URLUtils etc) is somewhere in a code generation step. You'll notice I've used interfaces to represent the re-usable parts of struct:

// *********************
// interface definitions
//
type URLUtilsIntf interface {
	URLUtils() *URLUtils
}

type HTMLElementIntf interface {
	HTMLElement() *HTMLElement
}

type HTMLAnchorElementIntf interface {
	HTMLAnchorElement() *HTMLAnchorElement
}

// *********************
// implementation definitions
//
type URLUtils struct {
	*js.Object `jsspecial:"true"`

	// interface URLUtils
	//
	Href     string `js:"href"`
	Protocol string `js:"protocol"`

	// ...
}

var _ URLUtilsIntf = new(URLUtils)

func (u *URLUtils) URLUtils() *URLUtils {
	return u
}

type HTMLElement struct {
	*js.Object `jsspecial:"true"`

	// interface HTMLElement
	//
	Title string `js:"title"`
	Lang  string `js:"lang"`

	// ...
}

var _ HTMLElementIntf = new(HTMLElement)

func (h *HTMLElement) HTMLElement() *HTMLElement {
	return h
}

type HTMLAnchorElement struct {
	*js.Object `jsspecial:"true"`

	// interface HTMLAnchorElement
	//
	Target   string `js:"target"`
	Download string `js:"download"`
	HrefLang string `js:"hreflang"`

	// ...

	// interface HTMLElement
	//
	Title string `js:"title"`
	Lang  string `js:"lang"`

	// ...

	// interface URLUtils
	//
	Href     string `js:"href"`
	Protocol string `js:"protocol"`

	// ...
}

var _ HTMLElementIntf = new(HTMLAnchorElement)
var _ HTMLAnchorElementIntf = new(HTMLAnchorElement)
var _ URLUtilsIntf = new(HTMLAnchorElement)

func (h *HTMLAnchorElement) HTMLElement() *HTMLElement {
	return &HTMLElement{Object: h.Object}
}

func (h *HTMLAnchorElement) HTMLAnchorElement() *HTMLAnchorElement {
	return h
}

func (h *HTMLAnchorElement) URLUtils() *URLUtils {
	return &URLUtils{Object: h.Object}
}

This isn't perfect by any stretch:

  • we now need some sort of tool to help in the code generation phase (not difficult but would need docs, distribution etc)
  • The "problem" of correct initialisation of the various "parts" moves to func (h *HTMLAnchorElement) URLUtils() *URLUtils etc (although it's admittedly clear what's going on)

On reflection I can definitely see arguments both ways... and as you've pointed out, we can't lose sight of the fact that any change from the status quo has a cost.

Thoughts/feedback welcomed...

@theclapp
Copy link

Any further thoughts on this, now that we've let it simmer for a while?

Also: Are we kind of dead in the water without a migration plan? If we provided a gofmt -r rewrite rule, would that be sufficient? I suspect not, but it'd be a good start.

@myitcv
Copy link
Member Author

myitcv commented Jul 10, 2017

@theclapp no further thoughts from me... but still keen to get thoughts from others if there are outstanding concerns/questions.

#617 is being worked on first in any case (per @shurcooL) but if we can round out discussion on this issue in parallel that would be a big help.

Are we kind of dead in the water without a migration plan?

Tooling is a must to my mind if we're making such a breaking change (and should be relatively easy to write, if gofmt -r doesn't cut the mustard that is)

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

7 participants