6 ms·
> (I might suggest the underlying problem in this code is relying on inference too much, but the threshold for "too much" is difficult to communicate to users.)
by heavenlyhash 7y ago
> (I might suggest the underlying problem in this code is relying on inference too much, but the threshold for "too much" is difficult to communicate to users.)
This is a very outstandingly interesting line out the whole writeup.
I like the writeup in its entirety for being very balanced and thoughtful, but this line in particular really stands out to me as worth more thought for anyone interested in language and type system design.
Inference is great.
Except... when it's not. When it's "too much". When it starts making breaking changes appear "too distantly".
It's an interesting topic to reflect upon, because the inference isn't making a breakage; just shifting around where the breakage appears. (And this makes it hard to ever direct criticism at an inference mechanism!) But this kind of shifting-around of the breakage appearance can be a drastic impact on the ergonomics of handling it: how early it's detected, how close to the important change site tooling will be able to point the coder, etc. That's important. And almost everything about it involves the word "too" -- which means the area is incredibly subjective, and requires some kind of norm-building... while not necessarily providing any clear place for that norm-building to center around.
I don't have a point here other than to say this is interesting to reflect on. I suspect the last chapter on type inference systems has not yet been written. Can an inference system be designed such that it naturally restrains "too" much use of it?
- the_duke 7y ago> because the inference isn't making a breakage; just shifting around where the breakage appears That's a very valuable insight, and a concern in a lot of languages with advanced inference! The problem is exacerbated in Typescript though because it is fundamentally a practical, evolved layer over Javascript, with an unsound type system that likes to bail out to any.
- jimbo1qaz 7y agoIn my experience with type inference (quite a bit of mypy, now learning rust), inferring local variable types based on what's assigned to it is not very confusing, as long as me or the IDE can figure out what type is being assigned. But when a function return value or variable's generic argument is determined by where the value is used, I tend to have more trouble (since one type can propagate from one function, to its argument, to the function which generated the argument).
- k__ 7y ago>this code is relying on inference too much Is this some TypeScript joke I'm too Hindley-Milner to understand?
- owl57 7y agoNot sure. Once upon a time I wrote a small program in Haskell for an exercise in cryptography. It defaulted to Word8 for all arithmetic throughout the program because some of the operands came from a ByteString. Scary. Maybe you are less too Hindley-Milner and more following the advice of explicit types on at least all top-level declarations?
- k__ 7y agoCould be, yes :)
- rat9988 7y agoThe real problem with shifting where it appears is when the error message is not helpful. With good error messages, it's not enough of a problem to justify leaving type inference in my opinion.
- pornel 7y agoThey pain of "unexpected any" they have doesn't come just from the type inference, but from gradual typing that makes it "too much". Without unknown/any being allowed to spread everywhere, the inference would have failed sooner, and the error/confusion would be contained (e.g. Rust stops on an ambiguous method call, plus limits implicit inference to function boundaries).
- klodolph 7y agoI feel this is a lesson every Haskell programmer goes through. Most Haskell programmers I know write types for all of the top-level declarations. Although it usually isn't necessary, it makes drastic improvements to the quality of error messages.
- rkangel 7y ago> Although it usually isn't necessary, it makes drastic improvements to the quality of error messages. It also provides valuable documentation. You can usually work out what a function does from just its type (and quite often, that's all you have to work with).
- papln 7y ago> just its type (and quite often, that's all you have to work with). which is a weakness in the culture of Haskell library authors and/or the ecosystem for contributing documentation patches.
- rkangel 7y agoCouldn't agree more. My recent experiences with Rust and especially Elixir have set high standards.
- fluffything 7y agoIIRC there are a couple of Rust write ups about this. In Rust, inference is limited within functions. The language doesn't allow inferring the argument or return types of functions to avoid this kind of action at a distance. The function API is just one of the many arbitrary places where one can limit inference and require users to provide types. In other languages the module boundary might also be an appropriate place to do that.
- cypressious 7y agoA big part of TS is the ease of transition from JS where you immediately get benefits through gradual typing and return type inference is probably a big part of that.
- jchw 7y agoThis is true, but a lot of projects are now starting with TS, and maybe could benefit from intentional limitations on inference, at least locally. I’m pretty sure most folks start new projects with strict enabled as is.
- breatheoften 7y agoIn my experience a lot of people don’t necessarily start projects with strict mode — and they _really really_ should ... most people I’ve talked to who couldn’t figure out how to use typescript never figured things out because they ended up somehow with a compiler configured with noImplicitAny: false — typescript really needs to find a way to change the default here ...
- WorldMaker 7y agoYes, my advice is almost always strict should be true for every new project, and even sometimes for migration projects where the end goal is to absolutely decrease tech debt and bugs as quickly as possible and the team isn't afraid of a giant compiler error waterfall as motivation. (It's a nice auto-generated TODO list with some semblance of progress reporting!) But at the very least, everyone should do themselves the favor and "noImplicitAny: true" no matter if it's new, migration, something inbetween, and no matter what they think of any of the other strict flags. Explicit anys are searchable TODO markers and fine, but implicit anys always seem to be hidden bugs waiting to be found.
- blauditore 7y agoI think the underlying problem is that in cases of long inference chains, it's hard to detect where the problem lies if types on both ends don't match. In my experience, the compiler will just show an error on the "latter" end, then the user needs to trace inference back and figure out where things went wrong. Personally, I experience this quite often, e.g. in Java whenever arrow functions are heavily used, such as with streams. It would be nice to have compilers report the whole inference chain somehow, just to be clear about all possible spots where things might be wrong. But I suppose that's difficult to visualize in a well human-readable way.
- pkolaczk 7y agoSome IDEs show type hints on hover, or if type mismatch is detected, they display inferred types inline in code. E.g Intellij Idea does that for Scala and it is really useful. I wish it did that in Java as well.
- papln 7y ago> But I suppose that's difficult to visualize in a well human-readable way. It's not. The type-checker must have constructed a call stack of inferred types in order to find the violation, so it simply has to print all the calls (source line and line number) and the types it deduced, and let the programmer (or IDE) compare that to the code in context and look for surprises.
- hollerith 7y agoJust to clarify: you're suggesting I believe that the programmer should be advised by the compiler to add one or two explicit type declarations to certain programs, not that the compiler should refuse to compile any programs that currently compile.
- pas 7y agoIt should be a compiler strictness flag.
- _bxg1 7y agoTypically my strategy is: 1) Make types explicit (or generic) at any interface boundary 2) Periodically query the inferred types of things from my editor just to make sure they come out to what I expect 3) If an error message feels like it's in the wrong place or an inferred type isn't what it should be, progressively make things more explicit until the problem is resolved But you're right, that's all very subjective and dependent on learned norms.
- lonelappde 7y agoThis is the same strategy I use for writing tests. This is natural, because types are compile-time tests.
- evmar 7y agoThanks for highlighting this bit, I also find it very interesting to think about! Here's a related property I've also discovered of 'advanced' type systems. In my understanding of how unification happens in systems like H-M, you first give every expression its own type variable, then perform a search to produce assignments to those that remain valid and leave the remainders generic. But as your type system gets fancier, and particularly in the presence of union types and subtyping, this search process can feel like "make whatever type judgements are necessary to make this program still compile". E.g. in code like let x = new Set(); x.add('hello'); x.add(3); // oops, meant to add the string '3' f(x); The inferencer can just reason "oh apparently x is Set<number|string>", and "oh apparently f() operates over all kinds of sets, Set<{}>". And especially when there's never a more specific type for things to "bottom out" on (like say f just forwards the set elements to JSON.stringify(), which accepts both string and number already), nothing will ever reveal to you that you actually wrote a bug. But then meanwhile even in Haskell you run into cases where the inferencer wasn't generic enough, like https://wiki.haskell.org/Monomorphism_restriction https://wiki.haskell.org/Monomorphism_restriction , so it's not even clear cut that you want the inferencer to be smarter or dumber. As my coworker says: as a programmer you have to be able to basically run the inferencer in your head, and that becomes very hard when the inferencer gets very smart. (See the RxJS bug in the above blog post.)
- whatshisface 7y agoThat problem has a simple solution: you, the programmer, should write type signatures in appropriate places as a combination of what documentation and assert statements do in other languages. No compiler today could be reasonably expected to guess which inferences the human brain will find surprising, which is why it's up to the programmer.
- evmar 7y agoThis is the point under discussion though -- humans think they're reasoning the same way as the inferencer but can't get it right in the limit, so they're not really equipped to know where they ought to have added type annotations. See e.g. the comment here about the inferencer working 'backwards': https://github.com/ReactiveX/rxjs/issues/4959#issuecomment-520629091 https://github.com/ReactiveX/rxjs/issues/4959#issuecomment-5... .
- rossjudson 7y agoYou should have let other people highlight this bit.
- WorldMaker 7y agoI also found it interesting because just as the later sections complain about cases where they relied on inferencing "too much", the earliest section also shows cases where they relied on inferencing "not enough". (Had the developer not explicitly typed by copying the inferred type from a tooltip for that D3.js return object, for instance, and left it to be inferred, it wouldn't have been broken in the generics change.) It's an interesting goldilocks paradox likely only exacerbated by the size of Google's codebase and variety of developer code styles/inferencing preferences.