Generate nicer-looking JS for common soak operations
See original GitHub issueIn #324, I implemented a first pass at soak operations that’s correct, but uses a __guard__
(or __guardFunc__
) helper function even in simple cases where it would look nicer to do something more direct. For example currentUser?.id
becomes __guard__(currentUser, x => x.id)
and a __guard__
function is added elsewhere in the file.
First, to establish some terminology:
- An expression is repeatable if we can be (mostly) sure that evaluating it has no side-effects. For example,
this.foo
is repeatable, butthis.bar()
is not repeatable. - The soaked expression is the left side of the soak operation. So in
a.b()?.c().d
, it’sa.b()
. - The soak container of a soak operation is the entire expression whose evaluation will be skipped if the soaked expression is null or undefined. For example, in
a(b.c()?.d.e())
, the soak container isb.c()?.d.e()
.
Here are some rules/heuristics that I think would be a good first start:
In non-nested soak operations, if the soaked expression is repeatable and the soak container is in a statement context, wrap the statement if an if
.
For example:
currentUser?.notify()
would become
if (currentUser != null) {
currentUser.notify();
}
(This might be controversial, since I bet some people would want currentUser && currentUser.notify();
, but in my opinion the if
is more clear when it’s a statement.)
In non-nested soak operations, if the soaked expression is repeatable and the soak container is in an expression context, wrap in a ternary or &&
For example:
userId = currentUser?.id
would become
let userId = currentUser != null ? currentUser.id : undefined;
or maybe
let userId = currentUser && currentUser.id;
I’m still a bit undecided on whether to use &&
. Might be worth a larger discussion. Of course, if we want to make it opt-in, we’d need a way of specifying preferences to decaffeinate, which doesn’t exist yet (to my knowledge).
But what if the soaked expression is not repeatable? If I understand right, the current makeRepeatable
code would do a transformation like this:
userId = UserManager.currentUser()?.id
into
let value;
let userId = (value = UserManager.currentUser()) != null ? value.id : undefined;
But it would be great if we could recognize that in this case it’s safe to pull the evaluation out into its own line. We could also do a better job of picking the variable name from the expression. So Ideally we could generate code like this:
let currentUser = UserManager.currentUser();
let userId = currentUser != null ? currentUser.id : undefined.
I think it’s always safe to pull the variable into its own line as long as the soak expression is in a statement context. If the soak expression is in an expression context, we could probably do some static analysis to determine some situations where it’s safe to initialize the variable when it’s declared (we can’t do anything that would change the order or number of times that any functions are invoked, though). But maybe that could be a general improvement to makeRepeatable
.
Another thought is that it’s probably still better to use a variable if the soaked expression is repeatable but really long. Maybe we could have a heuristic for that based on the number of characters in the expression.
Also, if the soak container is simple enough, the maybe
function mentioned in #176 could be nice. For example, UserManager.currentUser().getFriends().first()?.id
might be best written as maybe(UserManager.currentUser().getFriends().first()).id
.
Anyway, that’s a lot of different ideas, but I wanted to keep the discussion from #176 going. It may make sense to pull some of these into subtasks. Maybe a good scope of this issue is that 80-90% of soak operations in a typical codebase should convert to JS code that doesn’t use __guard__
. I think that’s reasonable, although it’s hard to say for sure.
Issue Analytics
- State:
- Created 7 years ago
- Reactions:3
- Comments:5 (3 by maintainers)
Top GitHub Comments
Not sure if it made more sense to put this in #1103 or here, but lodash has a lot of methods beyond
_.get
which can be very helpful for cleaning stuff up:_.invoke
does function calls on soaked expressions:a?.b.c(d)
is equivalent to_.invoke(a, "b.c", d)
._.set
does assignment on soaked expressions:a?.b = c
is equivalent to_.set(a, "b", c)
And
_.update
can do assignment based on the previous value, soa?.b += 3
is equivalent to_.update(a, "b", b => b + 3)
(Opinionated, but
_.isNil(x)
is an alternative tox == null
for replacingx?
, that I personally prefer, as I think== null
is a bit of a gotcha for less experienced JS developers and requires a specific linter exception)I believe any soak container can be expressed as a combination of these functions, it works in statement or expression context, and the end result is reasonably concise and readable and should produce identical results to the coffeescript. For example
a.b()?.c().d += 5
becomes:Right, if we wanted to handle that exact case, we could maybe compute a predicate like
isBeforeAnySideEffectsWithinStatement
or something. I think it should be possible to implement a defensive version of that by reusingisRepeatable
(although anisPure
would be more precise) and using the evaluation order. For example, the assignment expression case might be something like this:We could probably consolidate that logic by having each node expose the evaluation order of its child patchers or something.
But I also really like your idea of extracting multiple variables in a row, which would automatically handle the
isBeforeSideEffects
use case and more. It would be great ifmakeRepeatable
always (or almost always) did that kind of thing.One thought: maybe this logic could be moved to a post-processing step on JS code? I’m a bit scared of adding too much complexity to MainStage, especially since repeated magic-string operations can get really fragile. One idea is to leave
makeRepeatable
as-is (an inline assignment to a fresh variable), but choose a dummy name like__tmp1
, then pick a name for it once it’s in JS (which I think makes it a lot easier to use expression-namer?), and also move assignment expressions into their own statements as much as possible as a JS-to-JS transform.But you’re right that handling the easier cases is a good first step.