Replies: 14 comments
-
Documenting for a broader audience some of the discussion you and I have previously had about this topic, along with some new thoughts: XState's magic default for relative transition targetsI'm going to quote the machine declaration portion from your second sandbox here, as it is a succinct example of another dimension of the problem at hand: createMachine({
entry: () => console.log("entry root"),
exit: () => console.log("exit root"),
on: {
FOO: "b"
},
initial: "a",
states: {
a: {
entry: () => console.log("entry a")
},
b: {
id: "b",
entry: () => console.log("entry b")
}
}
}) The SCXML specification for i.e. as things stand today, for the
NOTE: I would still expect that an SCXML export from XState of this machine would end up explicitly adding a XState's configuration object as a DSLIn previous discussions, we ruled out the idea of changing the default to "internal" because it would deviate from the SCXML spec's default for the If we're being totally honest here, the JS API's configuration object has already taken a number of liberties in service of terse expressiveness in JavaScript. Ex. Any discussion as to whether we're deviating from the SCXML spec must first acknowledge that the JS API's configuration object is not a 1:1 mapping to SCXML tags. It is a DSL all its own, designed in the context of JavaScript specific syntax, idioms and conventions. (Which is also why that configuration object can not serve as a platform-agnostic interchange format, but SCXML can.) So, there is an argument to be made that this leaves open the option of changing the default for a transition when defined using that DSL. In point of fact, XState already does in order to introduce a sensible default for relative targets (see above). SCXML's choice of external as default, and the impact of LCCA behaviorWith regard to the first example, and the astonishing consequences of the default external transition in that machine, I just wanted to document what you and I already discovered:
SCXML <transition type="internal"> behaviorCuriously, the SCXML spec defines some interesting behavior around "internal":
tl;dr: if you specify "internal" for a transition where that is not valid, it acts like you specified "external" instead. 🤯 From this clue, I get the sneaky suspicion they might have entertained the idea of "internal" being the default at one point, especially since that default behavior would generally do what we've been thinking is the intuitive thing to do. To be clear, I don't like this at all, though. I wish they had introduced an "auto" option instead, and made explicitly specifying "internal" where it was invalid throw an error instead of silently doing the opposite. Choosing the best among bad options...For the purposes of interoperability across interpreters and other tools in the SCXML ecosystem, whatever the SCXML spec says is sacrosanct. Most of the places where XState's DSL has deviated are generally different enough to avoid confusion with any corresponding SCXML tag. However, in the situation where the DSL has a thing that is also called "transition" that has a "type" property with the same set of options as the SCXML Consequently, I think that the current relative-target-specific default is on shaky ground. Look at how drastically different the behavior is in your example above based on an easily omitted "." character. I don't think it is intuitive for one configuration object property's value to change the default of another property. I think a more apparent and flexible resolution to these concerns would be to introduce an "auto" option (as the default) in the DSL. I would expect this "auto" option to apply the most intuitive default behavior regardless of how you specify the target ('#b' vs 'b' vs '.b'). The transition type would be declared solely by the transition's With this change, the DSL has the opportunity to push users into the pit of success by default. To be clear, "auto" would be strictly a feature of the DSL. The visualization would render transitions as external or internal, and the SCXML export would output transitions with the resulting "internal" or "external" type. I'm not yet fully advocating the "auto" option above - just throwing this out there as another potential approach to resolving our shared concerns. |
Beta Was this translation helpful? Give feedback.
-
Just realized "intuitive" is really underspecified above. Scenarios I recall from our previous conversations:
For both, XState's existing examples typically steer you toward declaring a relative target, where you get a default transition type of internal. Given that, is this our heuristic for unsurprising default transition types? (i.e. the transition types that would be applied for "auto")
|
Beta Was this translation helpful? Give feedback.
-
I agree with @johnyanarella that XState is already its own DSL - heavily inspired by SCXML and we plan to maintain the ability to convert one to another as much as possible when it makes sense but at the end of the day it's a separate "language". More often than not people will use XState without prior knowledge of SCXML or UML or statecharts in general - I think we are not bound to preserve all defaults of the SCXML. And for people actually knowing about the prior art, it should be much easier to grasp the difference and learn about a different default from our docs. More than that - even Jim Barnett states that:
So it doesn't look to me that such a "change" would be fundamentally wrong - it would be a pragmatic choice. Re: internal behaving like external when targeting states outside of the source state. While maybe odd as it doesn't lead to people grasping both concepts as easily, it actually would be quite convenient for us to just use this as "auto". We wouldn't have to introduce any new terms to our own DSL (and code to handle it and other cases) which seems quite convenient as our goal is not to create as much to create a new language as it is to provide intuitive tools to solve problems. |
Beta Was this translation helpful? Give feedback.
-
Yeah, in the end, that ship sailed already, when it comes to the existing DSL. 😁 The API already does auto(I've been so deep in reading SCXML papers and thinking about alternative DSLs for it, that I forgot that the JavaScript DSL exposes an optional Agreed. In the docs, there's a long list of terse JavaScript DSL syntax that automatically become internal or external transitions—there's already an implied "auto" whenever
from:
The docs need some clarificationContradicting this, are these statements:
|
Beta Was this translation helpful? Give feedback.
-
I don't think it is a good idea to default to Otherwise, this is going to come back to bite you when:
|
Beta Was this translation helpful? Give feedback.
-
I think you can address the astonishing behavior for both "common handler" examples without introducing anything new to the DSL, by tweaking the "auto" behavior with some additional heuristics that inspect the states' relationships rather than just the target syntax. However, there are two obstacles:
|
Beta Was this translation helpful? Give feedback.
-
Based on what observable behavior this could trip the user?
This can actually just be drawn differently based on the relation of source and target states to avoid confusion (and not based on the |
Beta Was this translation helpful? Give feedback.
-
The first does presuppose that a developer has a bug in their machine (entry and exit actions are unexpectedly occurring on what they thought should be an internal transition, they're so confused now, and totally ready to flip their table over...) and they are using a debugger to inspect the state node properties of Granted, the XState inspector would (hopefully) be a better tool to debug this situation. Good warnings / lint errors would be even better. So, yes, you could derive the actual transition type as needed, but I thought we were all about making states explicit? 😉
By not wrangling that complexity (user's stated intent: auto vs internal vs external) at its source, it will propagate throughout the codebase.
Anyway, that's all internal implementation details, and you are way more familiar with the maintenance impacts and trade-offs in the JS engine implementation than me—this is all just a suggestion that might not make as much sense there. |
Beta Was this translation helpful? Give feedback.
-
Well - I see how maybe one could be a little bit confused when it comes to exit actions, entry actions on the other hand are rather hard to be confused as they would just be from a different part of the document. I don't mind surfacing "incorrect" internal transitions early - it makes sense to me. I'm only not sure if this is that much beneficial to include "auto" anywhere explicitly, it would require documentation and explanation for people that actually know SCXML/UML semantics. It's a new term to be learned. Wouldn't just treating lack of the |
Beta Was this translation helpful? Give feedback.
-
Intriguing... to your point about the DSL not necessarily requiring an understanding of SCXML semantics: What if the DSL exposed (New name might ease migration to new behavior, too.) Assuming the default behavior is now to be an internal transition when targeting oneself or children, and external for anything else, this would just be used for those edge cases where you do want to re-enter yourself or a child (use external instead) or explicitly not re-enter an ancestor (use internal instead). (Is that sound logic - can you use internal for an ancestor? Sorry, just woke up and no coffee yet!) And you could throw helpful errors if it was used for an invalid source / target configuration. The relationship to internal and external in SCXML could be explained in the SCXML section at the end of the relevant doc, as elsewhere. |
Beta Was this translation helpful? Give feedback.
-
I like it quite a bit!
Hmm, I would be for removing |
Beta Was this translation helpful? Give feedback.
-
Ah, yeah, was referring to the resulting transition behavior in the second quote. Exactly, suggesting “internal” as an attribute in the JS API config object goes away. |
Beta Was this translation helpful? Give feedback.
-
Good point that you want invalid I’d only suggest throwing an error for invalid use of the DSL “reenter” property. But, I definitely see why it gets tricky to distinguish, as you’re likely well past the origin of the value at the point where you’d have enough info to do that validation. |
Beta Was this translation helpful? Give feedback.
-
...or maybe it’s always a warning. Edit: oh, you said that! Yes, total agreement, then! |
Beta Was this translation helpful? Give feedback.
-
Consider this codesandbox with a machine such as:
This currently doesn't behave correctly in terms of SCXML because the defined transition is external and for external transitions, the LCCA state of the source and target states acts as the transition's domain. LCCA stands for Least Common Compound Ancestor.
What does it mean? It means that any descendant of the LCCA gets exited and reentered (if it is in the machine's configuration prior to transition). This is a very surprising behavior in this example because the source state is
s1
and it's a direct child of a parallel state. This means that its LCCA is actually the implicit root state (as parallel state cant act as LCCA) which in turn means thats2
should be exited and reentered as well. I highly doubt that this is intuitive behavior and it's pretty easy to get tripped by this - I would expect us to at least considering switching the default type of transitions to internal because it seems that this is what people usually expect and it should lead to a better "pit of success" feeling for our users.It seems to me that even a simpler example that doesn't involve any parallel states is rather surprising: https://codesandbox.io/s/silly-buck-68c26?file=/src/index.js
Beta Was this translation helpful? Give feedback.
All reactions