1. 20
  1. 6

    Since the article directly asks about “more languages,” Kotlin has a form of flow typing (smart casts) that, while it’s not as flexible as full-fledged pattern matching, is still useful in practice. It can be used to narrow values to arbitrary types, but in the code I’ve worked with, it’s most often used to narrow nullable types to non-nullable ones, including indirectly:

    // Kotlin denotes nullable types with a question mark suffix on the type
    // (not on the property the way TypeScript does it). So this is a class with
    // a single immutable property that might be null.
    
    class Demo(val msg: String?)
    
    fun tryFlowTyping(obj: Demo?) {
        // Here obj is of type "Demo?" and there may be no value of obj.msg
        // because obj itself may be null. Even if obj is non-null, obj.msg
        // still might be null.
    
        val temp = obj?.msg
    
        // If obj is null, or obj is non-null but obj.msg is null, temp will be
        // null at this point.
    
        if (temp != null) {
            // Since temp isn't null, and it is an immutable local variable,
            // the compiler can prove that obj isn't null and neither is obj.msg
            // (which is an immutable property). So obj is automatically cast
            // from type "Demo?" to type "Demo", and obj.msg is automatically cast
            // from "String?" to "String".
            println(obj.msg.length)
        }
    }
    

    The repetitive casting in the Java example, happily, will shortly be a thing of the past thanks to pattern matching for switch that’s currently available as a preview feature; it combines the “check the type and optionally some additional constraints” and the “assign to a narrowly-typed local variable” steps.

    1. 6

      Flow typing doesn’t work well with the designs of traditional ML-style languages. I’ll outline two reasons I am aware of

      ATS has ‘flow typing’ (refinement types), and it is a ‘traditional ML-style language’. I am surprised this is not mentioned. Refinement types are the PL-theoretical source that trickled down into the industry uses mentioned; but the mention of ML indicates at least some degree of academic awareness.

      1. 3

        One reason is most likely that it’s unsound in many cases. The following snippet is the standard Typescript version of this unsoundness:

        const dict: { x?: string } = { x: "here" };
        
        const removeX = () => { delete dict.x; };
        
        if (dict.x) {
          removeX();
          dict.x[0] // type checks even though x is now undefined
        }
        

        This is the reason why using the Map type in typescript doesn’t have flow typing.

        This problem gets even worse when you factor in concurrency (especially in the face of async/await).

        1. 2

          The same problems would be present in Haskell and OCaml - though, OCaml already has polymorphic variants, which are typed structurally. Why aren’t those flow typed? I’m not sure, but my inclination is that OCaml’s designers don’t want to complicate type inference rules or the compilation model

          Polymorphic variants are not flow-typed in the author’s sense, but you can (and routinely do) get something similar with as. Let’s use the author’s example of expected behavior flow-typing for variants:

          match light with               # light is Red|Yellow|Green here
              Yellow => do_yellow light  # light is Yellow here
              _ => do_other light        # light is Red|Green here
          

          In OCaml syntax:

          match light with
          | `Yellow => do_yellow light
          | _ => do_other light
          

          If you instead write

          match light with
          | `Yellow as y -> do_yellow y
          | (`Red | `Green) as other -> do_other other
          

          then y will have type [> `Yellow] and other will have type [> `Reed | `Green ]. In other words, it is possible to have pattern-matching refine type information on polymorphic variants, but it requires the user to explicitly introduce a new name for the refined value. (This satisfies the general principle that a given variable keeps the same type over its scope, which helps quite a bit getting predictable type inference, while allowing to write similar patterns that would require flow typing.)

          1. 1

            I’ve been using TypeScript for about a month, and flow typing is one of my favorite features. (And now I know what it’s called!)

            An interesting feature of TS is user-defined predicates that narrow types, like

            function isFoo(x: Foo | Bar) : x is Foo { return s instanceof Foo; }

            The return type x is Foo means the function returns a boolean, but with a compile-time side effect that if it returns true the compiler knows that the argument’s type is Foo.

            1. 3

              User-defined type guards are unsafe so you need to be careful with them. This’ll typecheck in a fully strict environment:

              const absurd = (x: unknown): x is number => true