The big problem with this take is that they seem to assume it is an all or nothing feature.
Personally I love how rust does it, types are inferred inside a body of a function where they matter less and are generally unambiguous (you need to specify them if they are ambiguous) as you often don’t care as much about the exact type you have. But on function parameters are return types they are never inferred as these form the contract to the rest of your program and a unexpected change to one of these can result in your code breaking somewhere else entirely.
Personally I never really use the automatic type annotations in IDEs, they just add noise I rarely care about.
I’m just glad that type inference can improve this sort of situation a bit:
ConfigManager configManager = new ConfigManager();
Why not just use auto (assuming C++) or var (assuming C#/Java)?
Because that’s type inference. The exact thing the article is arguing against. And that this comment is saying is nice.
You know what really sucks?
When I have a method that returns a
Foo
and like 300 places it gets called.And then I change it to return an ever so slightly different
Bar
Ah, now I need to go and update the code in 300 places cause the return type changed.
“Can’t you just
sed
your codebase?”Maybe, but it could cause some serious unintended dawizard
The solution to this problem (and many others) is to use an IDE / editor which supports refactoring like that. Which is pretty much every IDE / editor unless you’re using some very obscure language I think.
anything that supports your language’s language server protocol
Yup, that’s what I meant. I really don’t see why anyone wouldn’t use it nowadays.
I dont think external tooling should be a factor in deciding your language’s definition.
a lot of languages these days ship with tooling.
That doesnt change my point. The tooling is completely downstream of the language.
If it returned a “Foo”, whose structure changes in such a way as to requires changes in all places it was used…
That, sounds to me like a disaster you avoided by being helped (which is the polite way to describe developers not getting away with ignoring lazy and dangerous type conversion bugs) to fix each and every usage of it.
No, there’s countless ways code could be consuming a Foo or Bar and not care which.
Literally any form of serialization won’t care, for example.
Also you can change from a Foo to a Bar in a non breaking manner, where it’s name changed but the still have the same interface.
We’re talking about type inference, right?
If you have countless examples, I’d be happy to entertain others that have to do with type inference, because 1) serialisation is not related, 2) renaming in APIs is arguably, also not. Though, I cannot remember the last time my IDE wasn’t able to know, and do this for me.
Ah, now I need to go and audit the code in 300 places cause the return type changed.
Fixed
I can’t speak for OCaml, but type inference provides a lot of benefit in Rust. I already have too many keystrokes as it is, and forcing me to be explicit about everything would just add to the stress of using a keyboard.
I agree that types should be explicit at API boundaries. That’s precisely where you want to catch misuse.
As for the point about inference making code harder to read: I suppose that’s true if you spend a lot of time reading code outside of your editor where you also must know what the types are. But that just sounds like a bad workflow in general. Why are you avoiding using a better tool for the job? Modern code review tools like Github even support LSP-like features to solve this problem; and if your language isn’t supported… just pull the feature branch to review it.
He explicitly states that its not that bad in Rust because all functions must have type annotations. Its only a problem if your functions are huge (they shouldn’t). I think thats the correct way to go. Local variables can be inferred but anything else should be annotated.
Modern code review tools like Github even support LSP-like features to solve this problem; and if your language isn’t supported… just pull the feature branch to review it.
But now your requiring more tools and effort on the reviewer over, just reading the code.
But now your requiring more tools and effort on the reviewer over, just reading the code.
This should be completely negligible if you are writing code in the same code base.
I was already assuming I was working on the same codebase. I am not going to stash my work, checkout the branch and wait for the LSP to start up (if it’s working) just to confirm that your types aren’t doing anything weird. I’d rather just have them annotated correctly in the first place and just read the PR and trust the CI.
You don’t need to restart your LSP to switch to a new branch. You also don’t need an LSP to find the types.
Even with all of these issues aside, I can’t think of the last time I was reviewing a PR where it wasn’t clear from context what the types were, or they were irrelevant.
wth is your position then? If I can know the types from just looking at the code then it must have adequate type annotations and none of this matters. If I can’t tell the types and I have to pull the code locally to figure it out then I’m not starting the review on a good foot.
I think people here are thinking about type inference in a very local scope and not at a public function level which I understood the author to be complaining about.
If I can know the types from just looking at the code then it must have adequate type annotations and none of this matters
That’s not really true. It depends on the language. In Rust, it’s common to read a function without any explicit types except for the arguments and return type. So you may not know what types are used in the body without referring to the signatures of functions called.
If I can’t tell the types and I have to pull the code locally to figure it out then I’m not starting the review on a good foot.
It’s rare that knowing the types is critical to actually reviewing the code. Types are mostly for the compiler to help you. When reading the code, it’s more important that you see idioms, descriptive names, and test cases. There are rare occasions where it’s necessary to determine a type to resolve some ambiguity in how the code works, and in those cases, there are tools to help you do this (usually the same tools you use while writing code, e.g. LSP editor plugins and grep).
I think people here are thinking about type inference in a very local scope and not at a public function level which I understood the author to be complaining about.
In my very first comment, I said I can’t comment on OCaml. I am only really speaking on Rust here, where you have local inference and mandatory function type signatures.
It’s really weird to me to base any decisions around how much typing you have to do.
Typing is such a small part of programming I really don’t get it.
stress of using a keyboard
Can you elaborate?
Readability and maintainability are core imo.
It’s really weird to me to base any decisions around how much typing you have to do. Typing is such a small part of programming I really don’t get it.
Typing is a huge part of programming. Have you heard of RSI? People invest hundreds (sometimes thousands) of dollars in ergonomic keyboards just to overcome RSI pain. If you’re younger than 30 you might not be impacted by this, but many people who have been typing every day for over a decade are realizing it’s not sustainable without proper ergonomics.
Readability and maintainability are core imo.
I don’t think you sacrifice these by having local type inference. It’s never been an obstacle for me.
Like many things… it depends.
Type inference is wonderful for prototyping and writing short lived tools - it is an unnecessary expense for mature projects with a large number of developers on it.
At my shop we have a concept of “one off” routes that are written to accomplish a specific task and intended to be run once. A few of these are run repeatedly but with the understanding that these routes are unmaintained and exempt from automated testing requirements (we’ve got a separate bucket for routes that are only rarely invoked but are complex enough and frequently enough used to get test coverage). For stuff like those one off scripts I’ll never block a PR for omitting typing - while I absolutely will in our regular codebase.
I entirely agree. It all depends on context, preferences, goal and probably other things.
I found the article interesting even though I don’t entirely agree with all of it!
In Rust and Haskell you have to at least annotate the parameter types and return type of functions.
In OCaml type inference is a lot more powerful: you don’t have to annotate function signatures
Actually, Haskell and OCaml have this in common. Only Rust requires parameter types of the three.
I could do
add2 a b = a + b main = do putStrLn $ "5 + 3 = " ++ (show $ add2 5 3)
And that would work
Author has paper rejected by territorial reviewer who expects everyone to cite their shitty type inference research.
Author drops type inference diss track in response.
I prefer type inference. It’s extra clutter that can often be abstracted away with good variable and method names. If it quacks the way I need it then that’s one less thing I need to hold context of in my head.
You should read the article, because it’s pretty much a direct rebuttal with justifications to this exact argument. You’ve really just re-stated what the article disputes.
Which isn’t to say you’re wrong, I’d just be interested in your response to the arguments.
The article doesn’t make a persuasive case at all. It immediately backs off by acknowledging that 99% of type inference is fine, because it’s really only complaining about function signature inference, which is an extreme case that only a few obscure ML variants like Ocaml and F# support.
It’s like saying all american movies are terrible, and then clarifying that you’ve only seen White Chicks
I don’t want to infer types from my code. I’d rather infer the code from the types. Types are the spec, they are small and low in expressiveness, code is big and has infinitely more degrees of freedom than types. The bug surface area is smaller with types.
So it makes sense to use the types (simple, terse, constrained) to generate the code (big, unconstrained, longer to write, bug-prone). Inferring types from code is like building a complex machine without plans, and then using an X-ray diffractometer to extract plans from the physical object.
This is the argument.
This comes back to a perennially forgotten/rediscovered fundamental truth about coding: It is much easier to write code than read code
This is immediately followed by the next part that in any sufficiently large organization, you spend more time reading code than writing code.
Put it all together? Fractional second gains in writing that have meaningful expenses when it comes to reading aren’t worth it once you’re operating at any kind of scale.
If you and your buddy are making little hobby projects. If you have a 3 person dev team. If you’re writing your own utility for personal use… I wouldn’t expect these features to become evident at that scale.
Again, it isn’t saying that it’s something intrinsically wrong, it’s just that there is a trade off and if you really think about it, under most professional environments it’s a net negative effect on efficiency.
I agree if we’re talking at the granularity of function signatures, but beyond that I don’t. Every language supports type inference when chaining expressions. Inference on local variables is often a way of breaking up large expressions without forcing people to repeat obvious types.
As for inferring code from types, scrub the symbol names off any production java code and see how much sense it makes. If you really go down this path you’re quickly going to start wanting refinement types or dependent types. Both great research fields, but the harsh reality is that there’s no evidence that either field is production ready, or that either solves problems in readability.
The best technologies for reading code are all about interactive feedback loops that allow you to query and explore. In some languages that is type-based, with features like dot-completion, go-to-definition, and being able to hover to see types and doc comments. And just knowing whether the code compiles provides useful information.
In other languages, like Python and JavaScript, that feedback loop is value-based, and in some ways far richer and more powerful, but it suffers from being unavailable in most contexts. Most importantly, the lack of error messages is not a very useful signal.
I am obviously no authority, but my honest opinion is that type inference is completely orthogonal to the questions that actually matter in code readability, so blaming it is silly.
My response to the article is that you’re sacrificing gains in language because some people use outdated tools. Code has more context than what is just written. Many times you can’t see things in the code unless you dig in, for example responses from a database or key value store, or literally any external api. Type inference in languages that have bad IDE support leads to a bad experience, hence the author’s views on ocaml. But in a language like Kotlin it’s absolutely wonderful. If needed you can provide context, but otherwise the types are always there, you can view them easily if you’re using a decent IDE, and type inference makes the code much more readable in the long run. I would say that a majority of the time, you do not care about the types in any application. You care about the data flow, so having a type system that protects you from mismatched types is much more important that requiring types to be specified.
Maybe I’m missing something:
Does type inference provide a practical benefit to you beyond saving you some keystrokes?
What tools do you use for code review? Do you do them in GitHub/gitlab/Bitbucket or are you pulling every code review directly into your IDE? How frequently do you do code reviews?
Yes. Type-inference typically *knows better than me* what the types should be.
I frequently ask the compiler what code I need to write next by leaving a gap in my implementation and letting the compiler spit out the type of the missing section.
Can you explain why you wouldn’t know what a type should be?
lexer :: Parser LexState (Vector Int, Vector Token)
lexer = do
(positions, tokens) <- _ nextPositionedToken
…What goes where the underscore is in the above snippet?
I’ve never used Haskell, so I can barely read this as-is.
But sure: I have no idea, and I expect that’s your point.
You as the writer, you don’t know either? What if I could understand Haskell, is there an option to communicate that information to me? Or is the argument that nobody but the compiler and god need know? That having an awareness of the types has no value?
Does type inference provide a practical benefit to you beyond saving you some keystrokes?
it’s more readable! like, that’s literally the whole point. It’s more readable and you don’t have to care about a type unless you want or need to.
What tools do you use for code review? Do you do them in GitHub/gitlab/Bitbucket or are you pulling every code review directly into your IDE? How frequently do you do code reviews?
I use GitHub and Intellij. I do code reviews daily, I’m one of two staff software engineers on my team. I rarely ever need to know the type, and if I do Github is perfect for 90% of use cases, and for the other 10% I literally click the PR button in intellij and open up the pull request that way. It’s dead simple.
So you’re saying that for you, not only do you generally not see value is knowing types, but that them being explicitly defined is DETRIMENTAL to your ability to read the code?
For me, it’s like if I whip open a recipe book and see tomato sauce, dough, cheese, and pepperoni are the ingredients. Before the recipe details specifically how they are combined, I have a pretty good context from which to set expectations based on that alone. It’s a cheap way to build context.
But I don’t think you’re all lying. And you are very likely not all incompetent either. I wish I could sit down with you and have you show me examples of code where explicit types are detrimental to readability, so I could examine if there are cases that exist but are somehow being mitigated by a code style policy that I’m taking for granted.
That’s false for closures (or unnamed/inline) functions with context because their type is unique and so you just can’t write their type and that’s not a lang’s fault - that’s logically correct side-effect by-design.
To me this is an argument for why Go should not add type inference to function/method declarations. Go is like Rust (I guess, I haven’t used Rust) - type inference works for declaring a variable (or const) and generic type parameters but not for type declarations, methods, functions, etc. I was in the “more inference is always better” camp but now I’m thinking Go has the perfect level of inference. Except for function literals/lambdas. I really want go to infer the argument and return types when I’m passing a function literal/lambda to a function.
The thing about Rust’s type inference that seems wild to anyone who hasn’t seen Hindley-Milner/ML style type systems before is that it’s “bidirectional” (in quotes because that’s not a proper type theory term as far as I know). The type of the left-side of an assignment can determine the type (and behavior!) of the right side. For instance, this is ambiguous:
let foo = [("a", 1), ("b", 2)].into_iter().collect();
The expression creates an iterator over the
(letter, number)
pairs, andcollect()
stores the elements in a newly created container. But which container type? Here are two valid variants:let foo: Vec<_> = [("a", 1), ("b", 2)].into_iter().collect();
This creates a vector with items
("a", 1)
and("b", 2)
.let foo: HashMap<_, _> = [("a", 1), ("b", 2)].into_iter().collect();
This creates a mapping where
"a"
and"b"
are keys, and1
and2
are the corresponding values.Playground link in case you’d like to mess with this concept: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=76f999f4db600415643b0c58c19c69b7