| I work on a language, Dart, which also relies heavily on flow typing (which it calls "type promotion" because apparently every language needs their own name for it). We use it both for subtype tests and null checks. It's really nice and a large net win for Dart especially given its history. However, if I had a time machine and could redesign Dart from scratch, I would be tempted avoid flow typing and instead do something more like Rust and Swift do: Variables keep their static type but have pattern maching and nice syntactic sugar for simple use cases of it to make it easier to bind new variables with the narrowed type. The main problem is that flow typing is very complex, subtle, and can fail in ways that users find surprising. For example: foo(Object obj) {
closure() {
obj = "not int any more.";
}
if (obj is int) {
print(obj.abs());
}
closure();
}
This function is technically safe, but it's very hard for static analysis to reliably prove what kinds of flow analysis are valid when closures come into play. If that closure can escape, it can be impossible to prove that it won't be called before the variable is used.A simpler, more annoying example is: class C {
Object obj;
foo() {
if (obj is int) {
bar();
print(obj.abs());
}
}
bar() {}
}
This code looks like it should be fine. But if C is an unsealed class and some subclass overrides `bar()` to assign to `obj`, then the promotion could fail. Because of this, Dart can't promote fields and it causes no end of user annoyance. Top-level variables and static fields have similar limitations.Even when it works, it can be surprising: foo(Object obj) {
if (obj is int) {
var elements = [obj];
}
}
Should `elements` be inferred as a `List<Object>` or `List<int>`? What about here: foo(Object obj) {
if (obj is int) {
var elements = [obj];
elements.add("not int");
}
}
Should that `add()` call be OK or an error?Flow analysis is cool and feels like magic. It does the right thing 90+% of the time, but there's a lot going on under the hood that pops up in weird surprising ways sometimes. It's probably the right thing to do if your language has already invested in an imperative style. But if you have the luxury of defining a language from scratch, I think you can get something simpler and more predictable if you make it easier to define new variables of the refined type instead of mutating the type of an existing variable. Dart is heavily imperative, so I think flow analysis makes sense for it. Accommodating an imperative style is one of the main things that makes Dart so easy for new users to pick up, and that's an invaluable property. But I admit I envy Swift and Rust at times. |