Skip to content

Instantly share code, notes, and snippets.

@briangoetz
Created November 25, 2013 22:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save briangoetz/7650431 to your computer and use it in GitHub Desktop.
Save briangoetz/7650431 to your computer and use it in GitHub Desktop.
Flow typing
Proponents of "flow typing" have suggested, in code like this:
if (foo instanceof Bar) {
((Bar) foo).barMethod();
}
"why not just" allow the developer to elide the cast to Bar? Implicit in this question is the assumption that this is a very simple feature.
Like everything else, the easy cases are easy. The key question to ask is, under such a scheme, if the static type of foo outside the if-block is Moo, what is the static type of foo inside the if block?
One obvious, but wrong, candidate is Bar; this means that foo could no longer be used as a Moo:
if (foo instanceof Bar) {
...
foo.mooMethod(); // compile error: mooMethod() not a member of Bar
}
The less-wrong answer is an intersection type: (Moo & Bar). Now, imagine this overloading of existing methods:
void m(Moo m) { ... }
void m(Bar b) { ... }
and we have existing code like this:
if (foo instanceof Bar) {
m(foo);
...
}
But if we reinterpret the static type of foo as Moo & Bar, this code no longer compiles, due to an ambiguity in overload resolution. So this language change, as envisioned, is not source-compatible. Oops.
Now, one could layer arbitrarily complicated new rules for overload resolution to try and "fix" this problem, but now we're well outside the territory of an "easy" feature.
@mikaelgrev
Copy link

FWIW I never said it was easy. At least not intentionally. Sorry if I made it sound that way. I always try to be solution oriented and pragmatic.

I'm probably completely wrong but this was my intended solution when I suggested flow typing:

If the method/field that is being referenced isn't part of Moo's type then get the other types that it can safely be cast to by flow analysis. If exactly one would not create a compile error then do what's equivalent of a manual cast to that type, and we are done.

I haven't looked at the codebase for javac so I don't know if this is extra hard because of how it's made. There might also be corner cases, or more obvious problems that I have overlooked, that makes this particularly hard in Java but doable in say Kotlin or Ceylon.

I do understand that the proposal is dead though and I don't expect an answer. You probably have more pressing enhancements for Java 9/10 and I look forward to those. I just haven't seen any suggested thus far but I might on the wrong mailing lists.

I did ask Mark in the "Java future architectural" session at JavaOne where Oracle wanted to take Java. The "broad strokes" but I didn't get a clear answer. That might just mean that you (Oracle) want to hold your cards close to your chest and doesn't necessarily mean you are reluctant to evolve the language further.

Cheers,
Mikael Grev

@mikaelgrev
Copy link

Also, I was under the impression that LHS of instanceof actually needed to be a sub- or supertype of RHS or it wouldn't compile in the first place. And unless it's a subtype the whole expression can be disregarded by the compiler anyway. If so your whole key question is moot.

But again, I might have misunderstood this.

@rickyclarkson
Copy link

I would expect that instead of a direct intersection type, it could be done as a biased intersection, i.e., "use B's method iff there is no matching method on A", otherwise fail to compile with a good error message. I realise that's probably really hard to do in the compiler, but I can't see an obvious flaw. It sounds complex, but probably of the same level of complexity as the preservation of exception types that was added in Java 7 for rethrowing.

Besides that, would only allowing direct extends/implements relationships between A and B make this easier to implement and reason about?

That said, I'm not sure it's a feature that makes sense to add; lambdas could make such code easier, e.g., castAs(a, B.class, b -> do b-like things with it here), and casting is really something we should be trying to avoid rather than encourage.

@briangoetz
Copy link
Author

@Mikael: One of the most complicated parts of the Java Language Specification is method overload selection. So any feature that messes with it had better have a big payoff! (Like Lambda. In fact, a huge slice of the lambda work was overhauling the interaction between method overload selection and type inference, in ways that users will only see when it fails.) Also, its not just javac that has to implement overload selection; reflective scripting languages like EL or DynaLink have to emulate the behavior of overload selection. So adding weight here hurts more than just us.

If we implemented it as you suggest, we're basically hardcoding a search order into overload selection. But which should win in the event of a conflict like I suggested? Its not obvious; I would guess that whichever you hardcoded, there'd be cases where the use is legitimately surprised at the outcome.

Ceylon and Kotlin can get away with this precisely because they have no base of existing code with which they have to remain compatible. If we were more willing to break people's code when we came up with a new idea, it would be a heck of a lot easier to evolve the language. But then we'd also have way fewer users.

@ricky: Sure, you can go crazy with types. But it has a cost. Intersection types are well studied in the type theory literature (and still complicated enough); "biased intersection types" or whatever wacky thing we come up with would be a new thing. I'm wary enough of adding new type system magic; unproven type system magic seems even riskier.

Bottom line: we can add very few features, so we have to pick them carefully. Sometimes we come across a feature that is so small, simple, and interacts with nothing else (like binary literals) that it truly is easy. The rest of the time, we have to be very selective. A feature like this has relatively little incremental expressive power (compared to lambdas, generics, or pattern matching, for example) and still has plenty of complexity. Which suggests a bad return on complexity, hence my skepticism. We have to pick very carefully.

@mikaelgrev
Copy link

Actually, it would be nice to get a comment on the fact that foo needs to be a subtype of Bar to be compilable in the first place, or if I got that wrong, and how that affect your initial five minute assessment.

@briangoetz
Copy link
Author

You may be getting caught up on the distinction between the static and dynamic types of a variable? Consider:

Object x = new ArrayList<String>();

The static type (compile-time type) of x is Object. The dynamic (runtime) type of x is ArrayList. Many features depend on both static and dynamic types; for example, method overload selection is done off of the static type of the arguments; the overload is selected statically, and then dynamically dispatched on the runtime type of the receiver. But some only depend on static types, and some (like instanceof) depend only on dynamic types.

You don't need to have a static subtyping relationship for the cast to work; only a dynamic one. However, there are some casts that the compiler can statically prove cannot succeed (and IDEs and FindBugs will flag these for you.) For example:

ArrayList<String> x = new ArrayList<>();
if (x instanceof Serializable) { ... }  // might succeed

if (x instanceof LinkedList) { ... } // statically provable that this will not succeed

@mikaelgrev
Copy link

Ah, OK. Thanks for clearing that up.

I do of course know the difference but I didn't think that would matter here. Luckily you did. :)

Cheers,
Mikael

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment