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.
@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