Live data from Hacker News

Why don't more languages offer flow typing?

ayazhafiz.com

81–90 of 127 posts

Re: Why don't more languages offer flow typing?

#81
post #24

The answer is simple: because other languages don't need it - they have different features to deal with it. The author even mentions it: pattern matching. Just that he picks a language that doesn't support union-types. But that doesn't mean that flow typing would be necessary here - it means that the language(s) should support union-types and extend their pattern matching accordingly. In fact, I would say that flow t…

> In fact, I would say that flow typing is almost like a workaround for missing pattern matching. If flow typing is 'type narrowing' (the article kinda dragged on pulling in unrelated concepts so it lost me), then if anything, it is at least as good as pattern-matching/switch-blocks. At least in Typescript. That is because it works in switch statements and non-switch statements and provides the same guarantees. I don…

> It surely is nice to get the same guarantees without _needing_ a switch statement.

Is it?

    let data: Some(usize) = f();
    match data {
        Some(pages) => pages + 5,
        None => 0
    }
Seems pretty nice.

Or if you want a more structurally similar code with a guard (and removing the useless bits):

    let Some(pages) = f() else {
        return 0
    };
    pages + 5

Re: Why don't more languages offer flow typing?

#82

Nobody's mentioned dependent typing yet, but languages like Idris and Agda will "narrow types based on control flow" (in fact, since they're pure-functional, control-flow is the same as data-flow). For example, zipping two vectors of the same length: zip: Vec n t1 -> Vec n t2 -> Vec n (t1, t2) zip Nil Nil = Nil zip (Cons x xs) (Cons y ys) = Cons (x, y) (zip xs ys) Here the 'Vec' type has two constructors, Nil and Con…

This is amazing, but I don't understand the justification : > Since their types specify the same length, the type-checker knows we can ignore the 'zip Nil Cons' or 'zip Cons Nil' cases. Specifically, I have trouble understanding "their types specify the same length". Whose types ?

The types of the vectors: the length is a generic parameter to the type. So a Vec 4 Int and a Vec 5 Int are different types.

Re: Why don't more languages offer flow typing?

#83
post #27
post #16

Earlier quoted context omitted.

I was going to point this out, too. I thought it also supported cases like: if (foo instanceof Bar) { // foo is typed as Bar in this block } Is that true, or did I get that wrong?

if (o instanceof String s) { var foo = s.indexOf("bar"); } Java 17 introduced a preview of the same feature for switch statements return switch (o) { case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); default -> o.toString(); }; Future versions will have improved support: http://openj…

I use the `o instanceof Class c` thing all the time in my hobby compiler on Java 16 right now. The Java 17 version will greatly simplify much of my codebase.

Re: Why don't more languages offer flow typing?

#84

Earlier quoted context omitted.

You mean sum types, not union types (TypeScript has union types, your example uses sum types.)

An union type is the C attempt to implement something like sum types, that other languages extend a bit to literally implement sum types. Those two words live on different contexts, as unions are about a memory usage pattern, and sum types are about conceptual software design, but they often go together on the same language feature.

> An union type is the C attempt to implement something like sum types

Sum types, unions (in the C sense), and union types are three distinct concepts.

Wikipedia's article confusingly merges the latter two concepts together but they're different thing. TypeScript[1] and Scala[2] have union types, but they have nothing like C's notion of "unsafely reuse the same bits in memory to be interpreted as different things".

[1]: https://www.typescriptlang.org/docs/handbook/2/everyday-type...

[2]: https://dotty.epfl.ch/docs/reference/new-types/union-types.h...

Re: Why don't more languages offer flow typing?

#85

This question actually has a simple answer: Most languages have embraced statements over expressions for a lot of language constructs. This causes a lot of issues: An if-statement is in essence a function from boolean to unit/() (i.e. from a barely useful type to a type with no useful information), while an if-expressions will contain enough type information to at least provide this kind of “flow typing”, (even if it…

Almost all of the languages I know that do "flow typing" (TypeScript, Flow, Hack), "smart casts" (Kotlin), or "type promotion" (Dart) are fairly statement-oriented.

Expression-based languages tend to have pattern matching which provides another way to solve the same problem.

Flow typing is most useful in imperative languages where code like this is common:

    if (foo is! Bar) return "not a Bar";
    foo.someBarMethod();
Early returns and other imperative control flow is idiomatic and it's annoying if the static type system doesn't understand it.

In a more functional or expression-oriented language, early returns and other imperative control flow like that is rarer, so there's less "flowing" to type over.

Re: Why don't more languages offer flow typing?

#86
post #4

This is not a complete answer, but covers some languages. If you program too exclusively in dynamically-typed languages, you can be too used to not thinking about how physically large your types are, because you work in a world where everything is boxed, and allocations so plentiful you don't even hardly have a way of thinking about them because your language does them at the drop of a hat, and so on. But there are m…

Dynamic and strong typing are not opposed (dynamic and static typing are opposed, and, to the extent the distinction is valid, weak and strong typing are opposed), dynamic doesn't mean everything is physically boxed (most dynamic language implementations don't box some subset of small primitive values, usually including bools and small ints), and, in any case, flow typing is a feature of static type systems (though s…

I think swift combines sumtypes with flow typing in an interesting way, where if you use a guard statement it then propagates that information into future uses of the type. If I remember correctly Kotlin also does something similar.

Pattern matching and sum types help, but they solve different problems.

Re: Why don't more languages offer flow typing?

#87

Earlier quoted context omitted.

I really enjoy Typescript, but every time I use it I think to myself, this would be so much better if were built on top of something other than JavaScript. Maybe AssemblyScript or some other TS to WASM target will come along with a better underlying type system and a good standard library. Dare to dream.

Yep. Except that the base semantics of the language really are tuned for JavaScript. So I can't see a separation being feasible between the two. It would have to be a new language, sharing maybe 90% of syntax etc. but differing in some places where interoperability with JS has forced compromises (base number types is one thing I can think of. I'd like separate int and floats, etc.) and, yeah, targeting WASM etc. And…

What you're describing is essentially the origin story for Dart.

Re: Why don't more languages offer flow typing?

#88
post #45

Doesn't "flow typing" seem more like a bandage for the if-statement in languages that have nothing better. There is a language construct which is purpose built for unpacking sum types: The case expression (sometimes called match expression) case resp of Views n -> ... Error e -> ... Inside each branch, we have access to a value of the more specific type.

Why should type narrowing be limited to just case, and not both case and if? I'd argue that both of these are examples of flow typing.

My point was that if your language has a dedicated facility for case analysis, you don't really need to use if statements for that purpose. You just use case. That's one answer to "why don't more languages have flow typing".

Re: Why don't more languages offer flow typing?

#89

Nobody's mentioned dependent typing yet, but languages like Idris and Agda will "narrow types based on control flow" (in fact, since they're pure-functional, control-flow is the same as data-flow). For example, zipping two vectors of the same length: zip: Vec n t1 -> Vec n t2 -> Vec n (t1, t2) zip Nil Nil = Nil zip (Cons x xs) (Cons y ys) = Cons (x, y) (zip xs ys) Here the 'Vec' type has two constructors, Nil and Con…

This is amazing, but I don't understand the justification : > Since their types specify the same length, the type-checker knows we can ignore the 'zip Nil Cons' or 'zip Cons Nil' cases. Specifically, I have trouble understanding "their types specify the same length". Whose types ?

You might read the Vector type constructor 'Vector N T' as "this is a Vector of T's of length N'.

Re: Why don't more languages offer flow typing?

#90
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` or `List`? 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.

Post reply on HN