> Before we get to the harder ones, can people share what made them
> uncomfortable, for the ones where you marked "not comfortable with the
> answer”?
We’re currently exploring the consequences of Doug’s query about introducing
non-conditional locals into expressions (not necessarily because we want to
have such a feature now, but because if we did want such a feature, we’d want
the scoping machinery to handle it well, and if it doesn’t, that may suggest
refinements for the scoping machinery.)
Let’s talk about the more complicated examples in the quiz.
Everyone seemed to do pretty well on the if/else ones (flip the condition, and
it flips the scopes), the conditional-expression ones, and the
short-circuiting examples. Let’s talk about merging.
In this example:
```
public void test() {
if (a instanceof String v || b instanceof String v) {
println(v.length());
}
else {
println("foo");
}
}
```
we have two candidate conditional variables called `v` of type `String`, and
the flow rules ensure that at any point, at most one of them could be DA. So
in the `true` arm, the use of `v` is OK, and it binds to whichever of these is
DA (both must have the same type, otherwise its an error.) The rules sound
complicated, but as always, our DA intuition guides us pretty well — we can’t
get to the `true` arm unless exactly one of the `instanceof` expressions has
matched.
In the `&&` version of this example, people’s intuition was mostly right — that
this code is illegal — but some were unsure:
```
public void test() {
if (a instanceof String v && b instanceof String v) {
println(v.length());
}
else {
println("foo");
}
}
```
The answer here is that because `v` is not DU in the second `instanceof`, and
therefore might be in scope, this constitutes an illegal shadowing, and the
compiler rejects it.
The hard ones (and the potentially controversial ones) were the last two —
regarding use _after_ the declaring statement:
```
public void test() {
if (!(a instanceof String v))
throw new NotAStringException("a");
if (v.length() == 0)
throw new EmptyStringException();
println(v.length());
}
```
```
public void test() {
if (!(a instanceof String v))
return;
println(v.length());
}
```
In both of these cases, flow scoping allows `v` to be in scope in the last line
— because `v` is DA at the point of use. And this is because if the
`insteanceof` does not match, control does not make it to the use. In other
words, we can use the full flow analysis — including reachability — to conclude
the only way we could reach the use is if the binding is defined. (If there
was not a return or a throw, then the last use would not be in scope.)
For some, this is uncomfortable at first, as not only does the scope bleed into
the full `if` statement, but it bleeds out of it too. But again, we’re driven
by two goals:
- Make scoping line up with DA-ness (so we can build on an existing mechanism
rather than creating a new one)
- Be friendly to refactoring.
In the latter point, I’d like for
if (E) { throw; } else { s }
to be refactorable to
if (E) throw; s;
as it is common that users will perform precondition checking on entry to a
method, throw if there’s a failure, and then “fall” into the main body of the
method:
if (!(a instanceof String s))
throw new IllegalARgumentException(a.toString());
// use s here
The main argument against supporting this is discomfort; it feels new and
different. (But, because we’re building on DA, it’s not, really.)