← Back to context

Comment by magical_h4x

6 years ago

Nice article! It got me thinking about an issue I've noticed in dynamically typed languages (namely JavaScript) where it's very easy to end up doing lots of (potentially redundant) validation all over the place because it's much more difficult / unwieldy to pass that information along.

Everybody notices this. That's why most dynamically typed languages, after having reached good adoption, try to shoehorn type safety to fix that.

In this case not even TypeScript can rescue you. You might naively implement:

    const head = (input: A[]) => input[0]

and it will happily infer a return type of A. Then you'll fall flat on your face the first time you encounter an empty array:

    head([]) // -> undefined

To make it correct you need to explicitly define a return type of (input: A[]): A | undefined, just as the Maybe. It's obviously impossible for TS to guarantee that an array will not be empty at runtime, but I wish this specific case triggered some help from the type checker.

  • > It's obviously impossible for TS to guarantee that an array will not be empty at runtime, but I wish this specific case triggered some help from the type checker.

      type NonEmptyArray<T> = { head: T, rest: T[] };
    
      function makeNonEmptyArray<T>(array: T[]): NonEmptyArray<T> | null {
        if (array.length == 0) return null;
        return { head: array[0], rest: array.slice(1, array.length) };
      }
    
      function head<T>(array: T[]): T | null;
      function head<T>(array: NonEmptyArray<T>): T;
      function head(array: any): any {
        if (Array.isArray(array)) {
          if (array.length === 0) return null;
          return array[0];
        }
    
        if (typeof array === "object") {
          return array.head;
        }
      }
    

    http://www.typescriptlang.org/play/#code/C4TwDgpgBAcg9gOwKIF...

    It's impossible for Haskell to guarantee an array will not be empty at runtime as well; that's why we can write a new type + a smart constructor to track runtime properties.

  • > It's obviously impossible for TS to guarantee that an array will not be empty at runtime,

    Maybe you could use "Rest elements in tuple types" and do an overloaded signature like this:

        function head<T>(...args: [T, ...any[]]): T;
        function head<T>(...args: []): undefined
        function head<T>(...args: [T, ...any[]] | []): T | undefined
        {
            return args[0];
        }
    

    (not tested)

    You'd have to spread the arguments or use call/bind/apply, though.

    [1]: https://github.com/microsoft/TypeScript/pull/24897

Yep. For example you call:

    config.load_from_file(....

And you can't know if it check if the file exist first or not. From pure habit you add checks... (what? looking at the code of the library? thats crazy!).

  • The “exists” state is dynamic (something else in the system could be in the process of deleting the file since it was last checked). It’s more reliable if you don’t check in advance and simply try to open the file every time you need it. You handle errors while opening the file.

    • This is really interesting. For systems programming, files usually have clear ownership and modification context. If I call exists(), it should still exists for as long as that thread is paused.

      It's an anti-pattern to throw errors for expected behavior.

      2 replies →

Yeah, the author mentions that too.

For me, the funny thing about this article is that I kind of had the core insight just yesterday while hacking together a cli framework, but like the author, I couldn't quite explain what I learned.