-
Notifications
You must be signed in to change notification settings - Fork 570
Proposal: tightening up $internalize and $externalize (for zero values) #617
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
Comments
Thanks for reporting this and providing concrete details, @myitcv. I'll share some quick thoughts that come to me immediately after reading this, but I'll need more time to think about it further and perhaps I'll change my mind.
In my mind, it's not so clear whether it's a bug or not. It may, or may not be. It really depends on the following. Given this struct: type S struct {
*js.Object
A string `js:"A"`
} I know that And when you do: o := js.Global.Get("Object").New()
s := S{Object: o} Which is translated to something like It's very expected to me that The question is whether GopherJS should do a lightweight mapping/rewriting that is JavaScript-like, which it seems it currently does, or if it should do something more/different that is more Go-like (as you're expecting). I think @neelance's opinion on this is important, since it's a question of scope for this feature. Also, we should try to improve documentation of the feature so it's possible to determine correct behavior from reading the spec. (By the way, there's a good chance I overlooked something in your comment and repeated something you've already covered. Sorry if so, just let me know if that's the case.) |
@shurcooL I've just pushed up a PR that formalises roughly what I'm proposing. Namely to tighten up the existing The PR is intended, for now at least, to serve as a starting point for discussion. |
Possibly another way to look at this is: There simply is no way to map all JS values to Go values.
Go has no equivalent data type. So at most one of these behaviors can be copied. At present, the latter. I suspect that's somewhat by "accident" (by virtue of the fact that for all non-undefined values, stringification makes perfect sense) But I think a strong case can be made that the former makes more sense. The closest Go offers to For a little comparison, I amended @myitcv 's playground here with
The The So I guess TL;DR; I'm in favor of treating all undfined JS values as their Go zero values. But I think this is more a question of the spec (and thus, @neelance 's territory), than a clear cut-and-dried bug to be fixed. |
@flimzy, what about |
What I meant was that Go has no datatype that can be either a valid string, or undefined. Of course a *js. Object can do that, but a |
Mirroring Go's zero values sounds appropriate to me. Honestly even after ~7 years of writing JavaScript full time undefined is pretty much useless, I've always I haven't been using Gopher enough to know if there's any down side to that approach but it SGTM. I can see introducing a lot of bugs if you didn't know undefined == |
I'm trying to understand what underlying issue you're referring to. Let me ask the following questions. Do we agree that: type S struct {
*js.Object
A string `js:"A"`
}
s := S{Object: js.Global.Get("Object").New()}
fmt.Println(s.A) Is completely equivalent to: type S struct {
*js.Object
A string `js:"A"`
}
s := S{Object: js.Global.Get("Object").New()}
fmt.Println(s.Get("A").String()) Basically, If so, then it looks to me (but I could be wrong, please tell me) that your PR/issue can be simplified to: got := js.Undefined.String() // "undefined"
want := ""
got != want Basically, you're suggesting that the See this playground. |
Indeed, because they ultimately transpile down to the same Javascript: $internalize(s.A, $String) The issue I'm trying to address here is the value conversion semantics between the Javascript and Go worlds; specifically in this issue the direction It's my understanding that The example presented in the description highlights what to my mind is a gap in the spec/implementation that doesn't cover internalising of package main
import (
"fmt"
"github.com/gopherjs/gopherjs/js"
)
func main() {
o := js.Global.Get("Object").New()
o.Set("A", 5)
str := o.Get("A").String()
fmt.Printf("%q", str)
js.Global.Get("console").Call("log", "\"%s\"", str)
} This code should, to my mind, throw a runtime error (indeed I've just updated my PR to show what I believe the semantics should be in this case) |
I'm pretty sure I disagree with that last change/suggestion. That violates the way JS is meant to work, and would put a huge burden on the GopherJS coder to ensure runtime type safety before internalizing data. It's quite common that a JS lib will return a number or a string (or a stringable object). This change would require the coder to always check the JS data type, then do manual stringification. IMO its better the way it is now: If you care about the internal JS type, you can explicitly check. But in the common case, you can just internalize normally. |
To my mind the whole point of the The flip side is that if we don't do this then the fuzzy world of automatic type conversions leaks into the GopherJS world and makes it harder (impossible?) to easily reason about the resulting GopherJS code. If I have a I would much prefer the burden of having to "shim" some external Javascript code in such a way where I'm forced to be explicit about the conversions than rely on automatic conversions that allow my code to behave in ways. For example it just feels wrong to me that this should be possible: |
I can see and appreciate your view. It's one of the principal reasons I started with GopherJS: I hate the entire JS typing system. I'm still not sure I am in favor of such changes. But even if I was, this type of change would break a lot of existing code. Would it be possible to do this type of "strict" internalization in a separate package? It might even be possible to mimic the GopherJS struct tags. I haven't thought much about implementation of such a package yet. It's just a brainstorm for now. |
If we were to make this change then this is clearly a point that we would need to be understood more, but I'm slightly more optimistic in thinking it wouldn't actually break as often as we might think (but clearly some numbers would help steer this conversation 👍 ) Furthermore, if we were to make this change but as a developer you wanted the original (i.e. current at the time of writing) behaviour then you could simply: js.Global.Call("String", o.Get("A")).String() i.e. make explicit the fact that you want to use Javascript's automatic conversion. |
I guess I thought you were proposing that both forms of internalization would change. If you're only suggesting a change to the struct conversion, that bothers me less, but mainly because I personally don't use that much. I've found it to be too wrought with difficulties to be generally useful, so I tend toward the converter methods. |
I think (and might be wrong here) that every "form" ultimately transpiles to a call to |
Oh right, I see what you mean now. So the same potential for breakage still exists. |
To clarify, it's not that the https://gopherjs.github.io/playground/#/cj-JaeDcnP
So once you internalize a JavaScript object into a Go variable, it becomes a normal Go It's not yet clear to me whether making internalization conversion rules stricter would be helpful, but it might be. I can see it might be more impactful for I definitely agree that we should tighten up and document the internalization logic for |
Thanks @shurcooL, I had forgotten about these docs. Useful to have this as a reference for this discussion because the change I'm proposing would also have an impact for these methods. Specifically, in the case of
Again, the current behaviour can easily be achieved by performing the type conversion explicitly, as the docs try to show. But the key is that by forcing the developer to be explicit about where these type conversions are happening we:
|
I wonder if we can focus on just the nil/undefined/zero-value cases in this issue (in accordance with the title), and open a separate issue to discuss the merits of strict type checking of JS types (i.e. |
@flimzy that's a good point. I wonder however whether we end up having to take the two at the same time (and correspondingly change the title to reflect that) because they are so interrelated. At least discussing the two together in order that we work out where we want to end up... we might phase that as two separate changes in PR terms. |
I've just updated the related PR at #618 with the proposed docs change for |
From all of our discussion above, it seems there are two (related) issues/questions related to internalization of values from JS to Go:
I'd like to hear from @neelance on the above. That would make moving forward on this easier. |
Thanks @shurcooL - great summary. The only one change I would request is that |
Fair enough. I've added it now. The reason I skipped it originally is because I thought it was obvious that we'd want to internalize |
I just ran across another data point for this discussion, when using An uninitialized JS value mapped to |
I feel like having *js.Object in a struct is a clear signal that you are now dealing with javascript. The more go-like this becomes, the more mysterious *js.Object will be. My current use case is to create data objects in gopherjs that are then being passed to websocket connects etc so that I am both the producer and consumer of the data. In other words I already know what is going in and out so for me it doesn't matter at all. More generally I always fall on the side of trying to make implementations simpler so I don't see the need to make internalization stricter. |
@myitcv I know there's still some discussion/planning in the other proposals about structs with I'm tempted to move it to "NeedsFix" stage, but I'd really like to see a final and complete table of possible/impossible conversions (in both directions). From looking over this thread, I don't quite see that, not all in one place. I think we need that to be able to to implement this and test the implementation works as expected. Sorry, I should've communicated earlier what's needed to make progress on this, but I didn't get around to it until now. |
If you ask for a string of an "undefined" object, that it is returned as the string "undefined" is not an issue in my mind. Most of the code a person writes in gopherjs will adhere naturally to types. The intersection with external javascript libraries is unpredictable, I suppose, so it should be loose unless you expect the gopherjs user to make sure any old js library they are interacting with has a strict sense of types. |
@shurcooL 👍 - will work on the details. |
Made a bit of progress on this today. PR in #618 is updated. @shurcooL as requested, as part of flushing out the details for |
Another random data point I came across today, where GopherJS fails: pouchdb/pouchdb#6655 The PouchDB API is a case where strings and Date objects are, at times, used rather interchangeably. It's probably well beyond the scope of GopherJS to handle this situation gracefully, but it seems relevant to the spirit of the discussion just the same. |
Based on where things currently stand in #618, I've hit on something of an issue. Per the TODO list in the PR description, my choice of The
As you're aware, the compiler handles these methods as special cases; Under this (#617) proposal, use 1 is effectively hardened (within
This brings this use of o := js.Global.Get("Object").New()
fmt.Printf("%v", o)
Possible solutionsI'm not flush with ideas on this one right now, so more than anything adding this comment to get your thoughts. However, here's what came immediately to mind. Keep Create *js.Object.AsString()` and friends as follows for use case 1 to give us a method set of:
notice we drop These Would welcome your thoughts. |
I wish I could easily find where we discussed it last. But I raised the question of how In general, I think we should be more strict and less forgiving in order to make positive progress. I'll admit, it's hard for me to keep all the context related to this topic in my memory, and I don't have the bandwidth to dig more deeply into this right now. I hope you can make progress as is @myitcv, but let me know if you're blocked and need more to proceed. |
I think you're referring to the discussion in #625
Completely agree, indeed that was the motivation for this issue in the first place.
I understand the sentiment of this; we don't want to blow the scope of this piece of work unnecessarily. But at the moment I don't think the "do nothing" option is viable, i.e. I think we have to split the role of Consider: package main
import (
"fmt"
"honnef.co/go/js/dom"
)
func main() {
el := dom.GetWindow().Document().QuerySelector("script")
scr := el.(*dom.HTMLScriptElement)
fmt.Printf("%v\n", scr)
} This totally innocuous-looking code will in fact panic:
In this situation, we have a For this reason I don't think we can ignore or sidestep this issue. More details on the
|
My main concern here is of becoming too strict. The fast and loose typing of JS simply doesn't translate to Go's typing system easily, and runtime panics when one tries to convert between number/string, for example, seems very dangerous in some situations. I don't have a specific solution in mind (I already mentioned the idea of a tag for lax conversion rules in struct conversions, which seemed popular, but since this isn't about structs....) I suppose one possible solution might be a add an additional set of |
You will always have the option of doing this yourself as the caller. For example: v := js.Global.Call("eval", "5")
// s := v.AsString() - runtime exception
s := js.Global.Call("String", v).AsString()
The problem about not being strict is that we lose any sorts of ability to reason about the execution of our Go code... which is a problem. The canonical example of where this is a problem is in this comment |
I think we've discussed this before, and you already offered that work-around. My apologies for backtracking. Maybe it would be worth mentioning that in the new documentation, for those who want to retain the old/JS-style conversion behavior. |
That is an excellent suggestion, thank you! |
Uh oh!
There was an error while loading. Please reload this page.
Edit: the discussion has morphed into a proposal; details in this comment (which will continue to be updated)
This is really picking up conversation from #236, to register an issue for a specific problem, namely (playground):
The output of this is:
in both the browser console and the onscreen-Playground output.
To my mind this is a bug because the compiler generated code is failing to properly internalise an external value which we have defined to be of type
string
. That internalisation should handle the Javascriptundefined
value when we "read" the field value and convert it to the GopherJS""
value. The output should be:Similarly (and I note the discussion in #504) if the type of
A
were instead*string
then undefined would be converted tonil
.And in any situation where the conversion is not well defined (i.e. if the underlying field had a
number
value) then we panic.Furthermore the same conversion concept should apply for fields of all types on
*js.Object
-special types.The text was updated successfully, but these errors were encountered: