Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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 typing is almost like a workaround for missing pattern matching.



> 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't get this argument.

Consider something like:

    data: {pages: number} | null = f()
    if (!data){
       return 0
    }
    return data.pages + 5

Sure you can switch on the 'data' as well (exhaustive type-switches are a thing in Typescript too), but thats a syntactic preference. It surely is nice to get the same guarantees without _needing_ a switch statement.


> I don't get this argument.

Maybe it's because you focus on typescript here. But typescript is in a position that other languages are not: it has to make things work within an existing ecosystem, in particular with an untyped language. Under these constraints, flow typing might be the best solution. But that doesn't mean it's generally "at least as good as pattern-matching/switch-blocks".

Btw, pattern matching and switch-blocks are very different things - maybe it make it easier for you to understand to dive into a language that has native pattern matching and no switch-blocks at all because from what you write it seems to me that you have not been exposed to pattern matching much yet.


> 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 && <RenderNode node={node}/>}.


I don't think pattern matching and union-types makes narrow typing useless. Rust has both pattern matching and union types but still implements some specific forms of control-flow based typing.

For example, consider this Rust snippet :

    pub fn positive1(x: isize) -> Option<usize> {
        if x > 0 { Some(x as usize) } else { None }
    }
Unless I'm mistaken, without narrow typing, this cast would be impossible, and there would be no way to avoid the redundant runtime check caused by try_into().unwrap(), that is not even optimized unless you trick the compiler.


> Unless I'm mistaken, without narrow typing, this cast would be impossible

same sized integer casts in Rusts are no-ops [0]; the conditional isn't type narrowing, it just avoids the cases where the cast would not preserve the same semantic value.

[0] https://doc.rust-lang.org/reference/expressions/operator-exp...


Right. Thank your for the correction.


You need flow typing when you do pattern matching on generalized abstract data type (GADT). Here is an example in Java

    sealed interface Foo<T> permits Impl {}  // union type
    record Impl() implements Foo<String> {}  // product type

    <T> T m(Foo<T> foo) {
      return switch(foo) {
        case Impl impl -> "hello";  // flow typing T=String 
      };
    }


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


that's not the same as what is defined in the article, and it is supported by plenty of languages


Yeah, the same code would look much clearer in a language with union types. Heck even Swift cribbed them.

switch resp {

  case Result(val, meta):

    doStuff(val)

  case Error(msg, code):

    logger.main.debug(msg)

}


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


I did mean sum types. Thank you for the correction.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: