Live data from Hacker News

Why don't more languages offer flow typing?

ayazhafiz.com

91–100 of 127 posts

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

#91

Earlier quoted context omitted.

Something that's checked dynamically is not a "typing judgment", by definition. It's a proposition established at runtime, possibly even with non-trivial data attached describing "how" the dynamic check succeeds, and possibly affecting program operation in its own right as that proposition object gets "passed" to downstream functions that depend on that dynamic check. These are two altogether different things.

Sure. Tip: if something's true by definition, it's usually not interesting. Substitute the appropriate phrase - "observation of a property which, in the static context, might constitute a typing judgement"? Note that we're here specifically considering things that can be typed statically, as outside of that setting there is no question of whether static and dynamic are opposed. The rest of your comment seems concerne…

That probably reads a little snarkier than appropriate. My apologies for the tone.

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

#92
post #24

Earlier quoted context omitted.

> 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

> Seems pretty nice.

Never said it's not. I'm just saying it's nice to have type narrowing everywhere since it includes block based switching/matching as well (what you're arguging is nice, which I'd agree)

You'll have to use your imagination here because in typescript narrowing works on all conditionals (ternaries, boolean switches, etc), so naturally there are many more examples where it's useful. It's easy to trivialize any specific example.

E.g. they're useful in JSX when you want to do a one-line type-safe boolean switches like: {node && }.

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

#93

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…

You need flow typing when you do pattern matching on generalized abstract data type (GADT). Here is an example in Java sealed interface Foo permits Impl {} // union type record Impl() implements Foo {} // product type T m(Foo foo) { return switch(foo) { case Impl impl -> "hello"; // flow typing T=String }; }

By that definition, many (maybe most) languages then already support flow typing.

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

#94
Hafiz writes:

> almost every enterprise Java codebase I've had to work with has observed this pattern of testing whether an object is an instance of particular subclass, and using that to drive further computation. Even in the presence of such tests, Java demands that the author cast their objects appropriately. Of course you can always introduce an intermediate variable after the test, but I argue that this is still too much—upon verifying the instance of an object, the type system should be smart enough to update the object's type appropriately!

Because Java considers null to inhabit every type, in a Java project a few years ago, I handled this in a dynamic_cast-like way, as follows (abbreviated):

    public abstract class Security {     // ...
        public Stock asStock()   { return null; }
        public Future asFuture() { return null; }
    }

    public class Stock extends Security { // ...
        public Stock asStock() { return this; }
    }

    public class Future extends Security {
        public final SecurityTable factory;
        public final String exchange, symbol; // ...
        public Future asFuture() { return this; }
    }
This allows you to write relatively uncluttered type-dispatching downcasting code like this:

            public boolean contains(Security sec) {
                Future f = sec.asFuture();

                return f != null
                       && SecurityTable.this == f.factory
                       && f.symbol.equals(symbol);
            }
Of course this violates the open-closed principle (the abstract base class should be closed for modification) and official OO doctrine is that if you want different behavior for different subclasses you should put that behavior into a method that gets overridden by the subclasses, not in a conditional that attempts a downcast, so we did that a lot more often. But I found it a pleasant solution to the problem in the context of this Java project.

Of course it doesn't help in languages without implicit nullability, like TypeScript, which would ideally be all statically-typed languages.

— ⁂ —

The big difficulty with flow typing is, as I see it, not that it clashes with nominal typing; it's that it incorporates your compiler's static control flow analysis capabilities into your language's type system. Consider Hafiz's Java example:

        if (node instanceof DomNode.Element) {
            Layout layout = ((DomNode.Element) node).layout;
            return new RenderNode.Styled(layout, /* ... */);
        }
        return new RenderNode.Noop(/* ... */);
It is entirely reasonable to request that the type system handle this and allow you to write:

        if (node instanceof DomNode.Element) {
            Layout layout = node.layout;
            return new RenderNode.Styled(layout, /* ... */);
        }
        return new RenderNode.Noop(/* ... */);
But how about this? Now we're no longer inside the static extent of the `if`, and we're depending on the compiler to recognize the unconditional early return:

        if (!(node instanceof DomNode.Element)) {
            return new RenderNode.Noop(/* ... */);
        }
        Layout layout = node.layout;
        return new RenderNode.Styled(layout, /* ... */);
How about constant folding and partial evaluation?

        if (1 == 0 || node instanceof DomNode.Element) {
            Layout layout = node.layout;
            return new RenderNode.Styled(layout, /* ... */);
        }
        return new RenderNode.Noop(/* ... */);
Do we want to skip type checking entirely for dead code? Straightforwardly flow typing gives us that the type of every variable inside unreachable code is void, the uninhabited type, so any operation whatsoever on it is type-valid. Do we want the compiler to accept code like this?

        if (1 == 0) {
            Layout layout = node.layout + node / node;
            return new RenderNode.Styled(layout, /* ... */);
        }
        return new RenderNode.Noop(/* ... */);
And of course doing control-flow analysis precisely isn't feasible due to the halting problem; you need to do some conservative approximation.

So, if you want your programs to be portable from one version of the compiler to the next, somewhere you need to write down precisely what conservative approximation you're using for control-flow analysis, and in particular what you're not.

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

#95
post #55

Earlier quoted context omitted.

That's basically case 1, right? The compiler knows which type is being used at each call site, so it can generate a separate function for each type in the union and eliminate the type check/dead branches. I guess the exception would be if you have a non-homogenous array that you try to map over. In that case there's probably no way around boxing the values.

For consts and function arguments yes, I think. But you could have some mutable variable or field whose value depends on runtime state. In that case, you could have an actual polymorphic type.

But the previous example isn't that case.

if (c.type === 'a') { /* c is of type A here */ }

This is dynamic typing, this code is checked at runtime, and it's leveraged statically.

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

#96
post #17

Earlier quoted context omitted.

Isn't this sort of an orthogonal problem? Flow typing implies that your type is in some way unknown at the time the code fragment is evaluated. I think this can mostly happen in two situations: 1) The type is generic: A function may be called with a different type on each call site - but for each particular call site, the type is known at compile time. 2) The type is polymorphic - i.e. the full type is not known at c…

Sum types are often used in cases where the type is unknown at run time. In C++ the analog would be std::variant. In Rust it would be enums. In Haskell its algebraic data types. A first class sum type in c++ ala Rust or Haskell would certainly be appreciated, as the clunkiness of std::variant/std::visit is a well known annoyance.

Could you expand on your first sentence? I don't understand what do you mean that sum types are used when the type is unknown at runtime.

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

#97

Earlier quoted context omitted.

> Most languages have embraced statements over expressions for a lot of language constructs. Some of the most used languages with flow typing have statements and follow them for typing, so that is emphatically not the reason.

My view might of course be a bit outdated, do you have any examples? (Specifically for this point: “and follow them for typing”)

The example in TFA. TypeScript.

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

#98
post #55

Earlier quoted context omitted.

For consts and function arguments yes, I think. But you could have some mutable variable or field whose value depends on runtime state. In that case, you could have an actual polymorphic type.

But the previous example isn't that case. if (c.type === 'a') { /* c is of type A here */ } This is dynamic typing, this code is checked at runtime, and it's leveraged statically.

Yes, that's correct. In the GP example, c was const, so the type can be determined at compile time.

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

#100

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.

Yes and no.

Consider predicate types, or first-class null safety through unions.

If statements introduce propositions, which type systems could take advantage.

example:

  let idx = randomInt();
  if (0 
Post reply on HN