6 ms·
Flow tries to be sound and that makes it infinitely better than TS where the creators openly threw the idea of soundness out the window from the very beginning.
by hajile 2y ago
Flow tries to be sound and that makes it infinitely better than TS where the creators openly threw the idea of soundness out the window from the very beginning.
- machiaweliczny 2y agoBut in practice is was crap
- draw_down 2y agoMaybe FB should have tried putting more than like 4 people on the project then. (Yes I met them all once.)
- ahuth 2y agoTS made the choice to be “just JS” + types, and lean into JS-isms. Both choices are reasonable ones to make. Flow has some really cool stuff, and works great for a lot of people. There’s no denying, though, that there’s TS has done something right (even if you personally dislike it)
- idlephysicist 2y agoCan you explain what you mean when you say "to be sound"?
- pansa2 2y agoHere's an example of TypeScript failing to be sound - it should give a type error but it doesn't. I believe Flow does indeed give a type error in this situation: https://news.ycombinator.com/item?id=41069695 https://news.ycombinator.com/item?id=41069695
- recursive 2y agoYou don't have to go even that far to find unsoundness in flow. const arr = ["abcd"]; const str = arr[1]; const num = str.length; // this throws console.log(num); For me, typescript is a pretty good balance.
- fabiospampinato 2y agoWait, so Flow is not actually sound and their website is lying? Or do they have some "technically correct" definition of "sound" that takes stuff like that into account?
- recursive 2y agoI have no idea about the lawyerly technicalities, but you can try it yourself to verify what I'm saying. https://flow.org/try/ https://flow.org/try/ Compare these two programs. const arr = ["abcd"]; const str = arr[1]; const num = str.length; // this throws at runtime const arr = [new Date]; const dt = arr[1]; const num = dt.length; // fails to type check
- hajile 2y agoEven haskell will generate a runtime error for an out-of-bounds index main = putStrLn (["a", "b", "c"]!!4)
- recursive 2y agoThis is different. Neither flow, typescript, nor javascript generate a runtime error for an out of bounds index. It's explicitly allowed by the language. The result of the an OOB access of an array is specified to be `undefined`. The throw only happens later when the value is treated as the wrong type. I don't consider a runtime error to be a failure of the type system for OOB array access. But in javascript, it's explicitly allowed by specification. It's a failure of any type system that fails to account for this specified behavior in the language.
- hajile 2y ago> It's explicitly allowed by the language. This is like arguing that a null exception is fine because it's allowed by the language. If you get `undefined` when you expect another type, most future interaction are guaranteed to have JS throw because of the JS equivalent of a null pointer exception. They are technically different because a dynamic language runtime can prevent a total crash, but the effect on your web app is going to be essentially the same. [1,2,3][4].toFixed(2) > It's a failure of any type system that fails to account for this specified behavior in the language. Haskell has the ability to handle the error. How do you recommend a compiler to detect out-of-bounds at compile time? It can certainly do this for our trivial example, but that example will also be immediately evident the first time you run the code too, so it's probably not worth the effort. What about the infinite number of more subtle variants?
- price 2y agoWhen a language's type system is sound, that means that if you have an expression with type "string", then when you run the program the expression's value will only ever be a string and never some other sort of value. Or stated more abstractly: if an expression has type T, and at runtime the expression evaluates to a value v, then v has type T. The language can still have runtime errors, like if you try to access an array out of bounds. The key is that such operations have to give an error — like by throwing, so that the expression doesn't evaluate to any value at all — rather than returning a value that doesn't fit the type. Both TypeScript and Flow are unsound, because an expression with type "string" can always turn out to evaluate to null or a number or an object or anything else. Flow had the ambition to be sound, which is honorable but they never accomplished it. TypeScript announced up front that they didn't care about soundness: https://www.typescriptlang.org/docs/handbook/type-compatibility.html#a-note-on-soundness https://www.typescriptlang.org/docs/handbook/type-compatibil... Soundness is valuable because it makes it possible to look at the types and reason about the program using them. An unsound type-checker like TypeScript or Flow can still be very useful to human readers if most of the types in a codebase are accurate, but you always have to keep that asterisk in the back of your head. One very concrete consequence of soundness that it makes it possible to compile the code to fast native code. That's what motivated Dart a few years ago to migrate from an unsound type system to a sound one: https://dart.dev/language/type-system https://dart.dev/language/type-system so that it could AOT-compile Flutter apps for speed.
- dwb 2y agoThis is a point in Flow's favour. However! Seven years ago or so, when TypeScript was quite young and seemed inferior to Flow in almost all respects, I chose Flow for a large project. Since then, I spent inordinate amounts of time updating our code for the latest breaking Flow version, until one came along that would have taken too long to update for, so we just stayed on that one. We migrated to TypeScript a little while back and the practical effect has been much more and effective type checking through more coverage and support. TypeScript may be unsound, but it works better over all. We turn on the vast majority of the safety features to mitigate the unsoundness. And it's developed by a team that are beholden to a large and vibrant user base, so any changes are generally well-managed. There's no contest, really.
- hajile 2y agoTurning on EVERY safety feature won't mitigate the unsoundness because the problem goes beyond stuff like `any`.
- dwb 2y agoI know, but it just doesn't matter enough. Believe me, I'm signed up to the idea of theoretical rigour, the argument for soundness is part of what originally won me over (along with previous good experiences with Flow on a smaller project, and the support for gradual adoption). I will continue to be drawn to languages and tools that have a strong theoretical foundation. But in this particular case, today, when comparing these particular projects in the large JavaScript codebase I am talking about, TypeScript still wins by some distance. I promise that it has caught way more errors and been more generally helpful in its language server abilities than Flow ever was. Maybe Flow has caught up since then in its core functionality, I haven't been keeping track, but there would still be the wide disparity in community support which has serious implications for developer education, availability of library type definitions, etc.
- hajile 2y agoI think the real answer is adding actually sound types to JS itself. One of the biggest revolutions in JS JITS was the inline cache (IC). It allows fast lookup and specialized functions (which would be too expensive otherwise). These in turn allow all the optimizations of the higher-tier JITs. The biggest problem of Flow and TS is encouraging you to make slow code. The second you add a generic to your function, you are undoubtedly agreeing that it is going to be accepting more than 4 types. This means your function is megamorphic. No more IC. No more optimization (even worse, if it takes some time to hit those 5+ types, you get the dreaded deoptimization). In theory, they could detect that your function has 80 possible variations and create specialized, monomorphic functions for each one, but that's way too much code to send over the wire. That kind of specialization MUST be done in the JIT. If you bake the types into the language via a `"use type"` directive, this give a LOT of potential. First, you can add an actually sound type system. Second, like `"use strict"` eliminated a lot of the really bad parts of JS, you can eliminate unwanted type coercion and prevent the really dynamic things that prevent optimization. Because the JIT can use these types, it can eliminate the need for IC altogether in typed functions. It can still detect the most-used type variants of a function and make specialized versions then use the types to directly link those call sites to the fast version for even more optimization. I use TS because of its ubiquity, but I think there's the possibility for a future where a system a little more like Flow gets baked into the language.