Type inference
eli.thegreenplace.net
eli.thegreenplace.net
http://okmij.org/ftp/ML/generalization.html
The author starts with a naive implementation similar to the one linked here, then implements a series of optimizations that make it more efficient.
Compilers are good at deducing types from a function's implementation, but humans will find them easier to understand if they know what the function's inputs and outputs are supposed to be before attempting to figure out how the implementation does it. Error messages will be better, too.
Maybe it's trivial, but I found it helped me thinking about refactoring/reusing my code in contexts I didn't expect.
And what is this “end” you speak of? You don’t know what next year’s requirements are.
Don’t confuse the implementation with the contract. That’s a variant of duck typing. If you need to do stringy things to the data later don’t advertise genericity. People will start using your function and then you’ll have to make a new method because they own the contract now.
I agree that my point doesn't really apply to functions that are part of some kind of API.
Still, type inference is not duck typing: the former happens at compile time, unlike the latter.
> Is it that hard to figure out that your method is generic?
Well, it is not if you decide it to be generic from the start. What I was saying is that sometimes you start with "concrete" implementation, but the type inference helps you thinking about refactoring it to maximize reuse. Something like "hmmm, right now this method has signature 'a -> int -> 'b... I wonder if I can turn it into 'a -> ('b -> 'c) -> 'c and remove this other method that looks pretty similar..."
In languages that are still reasonably close to HM and that have decent editor tooling, like OCaml, leaving the type out in the implementation is the common practice. You only write them down in the external API (along with the documentation).
Clarify the domain so that you know the types of everything up front, and your work is more than halved.
"Write one to throw away" and all that.
Could typing out code be a good way to clarify the domain for some people, and/or some of the time?
Types aren’t always the right tool for that though. It would be interesting to see what could be done by integrating testing and typing, letting one inform the other.
Another issue I ran into recently was you can add a new type to your program, and break existing code. Where the type inference was able to deduce the type, and your new type appears to also be compatible, and now things don't compile and it is very mysterious why not!
There can be some interesting big wins by leaving types off when possible though, like super easy refactoring. Just start passing ints to a big chain of functions and you are done. No need to change the signature of a big chain of functions.
I've built a type inference system[0] for PHP and it's incredibly satisfying when it does what you expect.
So for me it makes more sense to have everything that’s beyond basic in something like a lsp-language server/ide.
I don't think that's true -- if you have principal types, you can just say in your language spec that the principal type is inferred.
[1] https://www.haskell.org/onlinereport/haskell2010/haskellch4....
On that topic, I found these notes to be very helpful: https://www.cs.cmu.edu/~fp/courses/15312-f04/handouts/15-bid...
But as I understand it, the concept started here: http://www.cis.upenn.edu/~bcpierce/papers/lti-toplas.pdf
For just the idea in this post, I suggest calling it something like "unification-based type inference", or "type inference using equational constraints".
http://jeremymikkola.com/posts/2018_03_25_understanding_algo...
So does the benefits outweight the drawbacks?
Subtyping is a fairly large domain, and it really depends what you mean. There are cases where it works fine (again, see OCaml) and other where it's more problematic (see Scala). In any case it's not really HM anymore, it needs to be more.
But the downside of type inference is that it makes reading code harder and you may depend on an IDE to know what your intermediate types are. Especially wrt generics and higher-order types where intermediate function calls don't have a concrete type in their signature.
Nothing demonstrates this better than any time you've written code yourself only to immediately invoke the IDE tooling to inspect what the type is. If you didn't know, good luck to the person reading it in git diffs. I've had to git clone someone's Rust project recently just to follow the type transformation across a bunch of future/stream chains.
This isn't an argument against static typing but rather an argument against inference excess. There's a nice middle ground where asserting the concrete type in critical places makes the code more readable but also checks your own assumptions. Like any time you received Maybe<Maybe<Int>> where you wanted Maybe<Int>.
(I mean "interfaces" not referring to any specific language feature, but to the concept in general).
Nit saying you arr wrong, but i just eant to point out that this happens even in languages like Java 8 which have no local type inference.
Any time you chain a method from another method, you are skipping a type. Ditto for inferred generic parameters on methods.
On the other hand, if you have strong enough types, it doesn't matter as much... if you don't know what the exact intermediate types are, you can still know it's correct based on the final type and the fact that it compiled, which is enough for a code review imho.
type inference is that it
makes reading code harder
I find the opposite to be the case. My syntax isn't polluted with all manner of superfluous syntactic noise like A a = new A
How is that more readable than something like the following? a = new A
There are edge cases, but type-inferred languages do allow you to add type annotations. Especially wrt generics
Why is this a problem? The very essence of parametric polymorphism is that the do "the same thing" at all types. This was first shown with Reynolds' famous Abstraction Theorem [1], which has in the mean time, been generalised in many ways.[1] J. C. Reynolds, Types, abstraction, and parametric polymorphism.
export const mergeItems = (keyExtractor, oldItems, newItems) =>
uniqBy(oldItems.concat(newItems), keyExtractor)
What is keyExtractor? What are old and new items? You can probably guess that oldItems and newItems are an array of something, and you can probably infer from names and stuff that keyExtractor should extract a key from the items somehow, but aside from that you don't have much. If you didn't know what the lodash function "uniqBy" does, you wouldn't know how to use this without research.Contrast that with this below (Javascript with Flow type annotations):
export const mergeItems = <Item> (
keyExtractor: (value: Item) => string,
oldItems: Array<Item>,
newItems: Array<Item>,
): Array<Item> => uniqBy(oldItems.concat(newItems), keyExtractor)
With types it is a lot more explicit. Key extractor is given the item in the array, and should return a string key. Now you don't need to know or care about how "uniqBy" works, or the exact details of how you should or shouldn't use it, and if you try to pass 2 arrays with different items, it will yell at you because that's not right, something that type inference can't determine (at least not with Flow's typesystem) look at this simple function
As I said, type-inferred languages do allow you to add type annotations when they are helpful. Note that your example has two arguments of the same type Array<Item>. In this case your even explicit typing doesn't prevent mixing up oldItems and newItems. In this case you want named parameters ...Finding out type definitions is made massively easier with an IDE, but even without one there's an extremely high chance any library you're using is going to at least have autogenerated docs sitting on disk somewhere or mirrored on a crappy website. Maybe you need to clone their repo and build the docs yourself, but even that's pretty unlikely as most language ecosystems have a package manager + docs website or man pages.
I don't really develop anything without at least a third of my screen real estate dedicated to documentation and I don't see "RTFM" as a meaningful or undesirable barrier to programming correctly.
> Now you don't need to know or care about how "uniqBy" works, or the exact details of how you should or shouldn't use it...
I don't follow you. What if "uniqBy" only works properly on sorted lists? You'd never know that based on the type (unless you've got dependent types) and if you're not reading docs on how the function works (the types check/it compiles!) you're going to be in a world of hurt at runtime. Intuitive programming may be easier but it's a heck of a lot more dangerous unless your compiler is intuitive too (i.e. makes the same intuitive assumptions that you do).
> ... and if you try to pass 2 arrays with different items, it will yell at you because that's not right, something that type inference can't determine (at least not with Flow's typesystem)
That's entirely Flow's fault. There's no reason a "proper" type system couldn't deduce that the same type would be needed for both arrays, the `oldItems.concat()` call should have a type definition something like (functional for brevity): concat(Array<T>, Array<T>). Nothing ambiguous about T being the same type.
Types are an additional layer on top, that can help provide context and information which is often difficult to get across in "plain english" in documentation, and they have the added benefit of being machine readable and verifiable so that a mistake that us humans make or an instance where we forget an edge case is more likely to be caught.
Before I used Flow, the documentation above a function like that would probably look something like this:
/**
* [normal docs here about what it does and where it's used]
*
* keyExtractor is a function that will be called for each element in both the
* new and old array, and it should return a string key that will uniquely
* identify the element so that the combined array can be de-duplicated.
*/
It's not impossible to write out, and to be honest it probably wouldn't hurt to still add some of that into the function even if it's typed, but it's a lot more writing to get the point across, and it can be ambiguous, can easily become "desynced" with the code itself, and won't show up in IDE tooling when going to use the function (or if it does, it won't be easily parsable like a small type would be).I'm not saying types let you use a function without knowing anything about what it does or how it's written, but that it makes it easier to just remember the basic ideas and it lets the type system catch some mistakes or problems, while also providing some additional "documentation".
>That's entirely Flow's fault.
Don't worry, I was wrong anyway! Even without typedefs it will error if you pass in arrays of 2 different types. But I believe some type systems will try to infer that the function is generic and can take either, and without explicitly defining the type constraints, will over-genericise (is that a word?) the function to allow it to allow more than the user expected it to.
A good type system in programming is both an additional safety net as well as a guiding hand. It doesn't replace documentation, but augments it, and makes it better, and can bail you out if you get it wrong (sometimes). I once heard types referred to as a "checksum" for your function. It can feel like you are just re-stating the obvious, but the compiler will verify that the implementation and the types are in agreement, and that helps prevent some kinds of bugs.
And also an argument for exposing better tooling when reading git diffs :D