34

It seems that all new programming languages or at least the ones that became popular use type inference. Even Javascript got types and type inference though various implementations (Acscript, typescript etc). It looks great to me but I'm wondering if there are any trade-offs or why let's say Java or the old good languages don't have type inference

  • When declaring a variable in Go without specifying its type (using var without a type or the := syntax), the variable's type is inferred from the value on the right hand side.
  • D allows writing large code fragments without redundantly specifying types, like dynamic languages do. On the other hand, static inference deduces types and other code properties, giving the best of both the static and the dynamic worlds.
  • The type inference engine in Rust is pretty smart. It does more than looking at the type of the r-value during an initialization. It also looks how the variable is used afterwards to infer its type.
  • Swift uses type inference to work out the appropriate type. Type inference enables a compiler to deduce the type of a particular expression automatically when it compiles your code, simply by examining the values you provide.
5
  • 3
    In C#, the general guidelines says to not always use var because it can sometimes hurt the readability.
    – Mephy
    Commented Feb 3, 2015 at 2:49
  • 5
    "...or why let's say Java or the old good languages don't have type inference" The reasons are probably historical; ML appeared 1 year after C according to Wikipedia and it had type inference. Java was trying to appeal to C++ developers. C++ started as an extension of C, and C's primary concerns were being a portable wrapper over assembly and being easy to compile. For whatever it's worth, I've read that subtyping makes type inference undecidable in the general case as well.
    – Doval
    Commented Feb 3, 2015 at 3:14
  • 3
    @Doval Scala seems to do a pretty good job at inferring types, for a language that supports subtype inheritance. It's not as good as any of the ML-family languages, but it's probably as good as you could ask for, given the language's design.
    – KChaloux
    Commented Feb 3, 2015 at 16:15
  • 13
    It’s worthwhile to draw a distinction between type deduction (monodirectional, like C# var and C++ auto) and type inference (bidirectional, like Haskell let). In the former case, the type of a name may be inferred from its initialiser only—its uses must follow the type of the name. In the latter case, the type of a name may be inferred from its uses also—which is useful in that you can write simply [] for an empty sequence, regardless of the element type, or newEmptyMVar for a new null mutable reference, regardless of the referent type.
    – Jon Purdy
    Commented Feb 5, 2015 at 8:55
  • Type inference can be incredibly complicated to implement, especially efficiently. People don't want their compilation complexity to suffer exponential blowup from more complex expressions. Interesting read: cocoawithlove.com/blog/2016/07/12/type-checker-issues.html
    – Alexander
    Commented Sep 20, 2017 at 4:08

8 Answers 8

51

Haskell's type system is fully inferrable (leaving aside polymorphic recursion, certain language extensions, and the dreaded monomorphism restriction), yet programmers still frequently provide type annotations in the source code even when they don't need to. Why?

  1. Type annotations serve as documentation. This is especially true with types as expressive as Haskell's. Given a function's name and its type you can usually have a pretty good guess at what the function does, especially when the function is parametrically polymorphic.
  2. Type annotations can drive development. Writing a type signature before you write the body of a function feels kind-of like test-driven development. In my experience, once you make a Haskell function compile it often works first time. (Of course, this does not obviate the need for automated tests!)
  3. Explicit types can help you check your assumptions. When I'm trying to understand some code that already works, I frequently pepper it with what I believe to be the correct type annotations. If the code still compiles I know I've understood it. If it doesn't, I read the error message.
  4. Type signatures let you specialise polymorphic functions. Very occasionally, an API is more expressive or useful if certain functions are not polymorphic. The compiler won't complain if you give a function a less general type than would be inferred. The classic example is map :: (a -> b) -> [a] -> [b]. Its more general form (fmap :: Functor f => (a -> b) -> f a -> f b) applies to all Functors, not just lists. But it was felt that map would be easier to understand for beginners, so it lives on alongside its bigger brother.

On the whole, the downsides of a statically-typed-but-inferrable system are much the same as the downsides of static typing in general, a well-worn discussion on this site and others (Googling "static typing disadvantages" will get you hundreds of pages of flame-wars). Of course, some of said disadvantages are ameliorated by the smaller quantity of type annotations in an inferrable system.

If you’ll permit a little navel-gazing: We compiler writers like to think of a program as a document that is written in full, and then read (by a compiler or by another programmer). Code is read more often than it’s written. In that worldview type inference is a great UX - it allows the document’s author to leave out a load of boring bookkeeping which a compiler can figure out (and a reader probably doesn’t care about). On the other hand, the language matters most when a program is half-done. A type that’s been written down (and not inferred) is a type that can actively participate in the act of programming - think about workflows like IDE autocomplete or hole-driven development.

Java* proves that a language requiring too many type annotations gets annoying, but with too few you lose out on the advantages I described above. Languages with opt-out type inference strike an agreeable balance between the two extremes.

*Even Java, the scapegoat, performs a certain amount of local type inference. In a statement like Integer z = foo(bar(x), baz());, you don't have to annotate every subexpression with their types - that’d be unusably cumbersome! But JVM bytecode is very explicitly typed. The compiler infers (and retains) those annotations for you during type checking. On the other hand, ML-style languages are typically globally inferrable.

18
  • 2
    But you're not a beginner ;) You have to have an understanding of type classes and kinds before you really "get" Functor. Lists and map are more likely to be familiar to non-seasoned Haskellers. Commented Feb 3, 2015 at 8:48
  • 1
    "assuming you know the type of the variables, you know which method gets called and thus the return type" <- You just described an algorithm for type inference in such an expression. It's a simple algorithm because it's a simple situation, but inference is happening nonetheless. In the absence of all type inference, that expression would look like (Foo)foo((Bar)bar((Baz)baz), (Quux)quux()). Commented Feb 3, 2015 at 22:06
  • 1
    Would you consider C#'s var to be an example of type inference? Commented Feb 3, 2015 at 22:17
  • 1
    Yes, C#'s var is a proper example.
    – Doval
    Commented Feb 3, 2015 at 22:19
  • 1
    The site is already flagging this discussion as too lengthy so this'll be my last reply. In the former the compiler must determine the type of x; in the latter there is no type to determine, all types are known and you just have to check that the expression makes sense. The difference becomes more important as you move past trivial examples and x is used in multiple places; the compiler then has to cross-check the locations where x is used to determine 1) is it possible to assign x a type such that the code will type check? 2) If it is, what's the most general type we can assign it?
    – Doval
    Commented Feb 3, 2015 at 22:25
8

In C#, type inference occurs at compile-time, so the runtime cost is zero.

As a matter of style, var is used for situations where it is either inconvenient or unnecessary to manually specify the type. Linq is one such situation. Another is:

var s = new SomeReallyLongTypeNameWith<Several, Type, Parameters>(andFormal, parameters);

without which you would be repeating your really long type name (and the type parameters) rather than simply saying var.

Use the actual name of the type when being explicit improves code clarity.

There are some situations where type inference cannot be used, such as member variable declarations whose values are set at construction time, or where you really want intellisense to work properly (Hackerrank's IDE won't intellisense the variable's members unless you declare the type explicitly).

4
  • 18
    "In C#, type inference occurs at compile-time, so the runtime cost is zero." Type inference occurs at compile-time by definition, so that's the case in every language.
    – Doval
    Commented Feb 3, 2015 at 3:05
  • So much the better. Commented Feb 3, 2015 at 3:06
  • 2
    @Doval it is possible to perform type inference during JIT compilation, which does clearly have a runtime cost.
    – Jules
    Commented Feb 3, 2015 at 9:37
  • 6
    @Jules If you've gotten as far as running the program then it's already been type checked; there's nothing left to infer. What you're talking about isn't typically called type inference, though I'm not sure what the proper term is.
    – Doval
    Commented Feb 3, 2015 at 12:55
7

Good question!

  1. Since the type is not explicitly annotated, it can at times make the code harder to read - leading to more bugs. Properly used it of course makes the code cleaner and more readable. If you're a pessimist and think that most programmers are bad (or work where most programmers are bad), this will be a net loss.
  2. While the type inference algorithm is relatively simple, it is not free. This sort of thing increases compile time slightly.
  3. Since the type is not explicitly annotated, your IDE can't guess as well what you're trying to do, harming autocomplete and similar helpers during the declaration process.
  4. Combined with function overloads, you can occasionally get into situations where the type inference algorithm can't decide which path to take, leading to uglier casting style annotations. (This happens quite a bit with C#'s anonymous function syntax for example).

And there are more esoteric languages that cannot do their weirdness without explicit type annotation. So far, there are none that I know of that are common/popular/promising enough to mention except in passing.

6
  • 1
    autocomplete doesn't give a shit about type inference. It doesn't matter how the type was decided, only what the type has. And the problems with C#'s lambdas are nothing to do with inference, they are because the lambdas are polymorphic but the type system can't cope with it- which has nothing to do with inference.
    – DeadMG
    Commented Feb 3, 2015 at 9:26
  • 2
    @deadmg - sure, once the variable is declared it has no problem, but while you're typing in the variable declaration, it can't narrow the options of the right hand side to the declared type since there is no declared type.
    – Telastyn
    Commented Feb 3, 2015 at 12:44
  • @deadmg - as for the lambdas, I'm not super clear on what you mean, but I'm pretty sure it's not entirely correct based on my understanding of how things work. Even something as simple as var foo = x => x; fails because the language needs to infer x here and has nothing to go on. When the lambdas are built, they're built as explicitly typed delegates Func<int, int> foo = x => x is built into CIL as Func<int, int> foo = new Func<int, int>(x=>x); where the lambda is built as a generated, explicitly typed function. To me, inferring the type of x is part and parcel of type inference...
    – Telastyn
    Commented Feb 3, 2015 at 14:15
  • 3
    @Telastyn It's not true that the language has nothing to go on. All the information needed to type the expression x => x is contained inside the lambda itself--it doesn't refer to any variables from the surrounding scope. Any sane functional language would correctly infer that its type is "for all types a, a -> a". I think what DeadMG is trying to say is that C#'s type system lacks the principal type property which means that it's always possible to figure out the most general type for any expression. It's a very easy property to destroy.
    – Doval
    Commented Feb 3, 2015 at 14:30
  • @Doval - enh... there's no such thing as generic function objects in C#, which I think is subtly different than what you're both talking about even if it leads to the same trouble.
    – Telastyn
    Commented Feb 3, 2015 at 14:35
4

It looks great to me but I'm wondering if there are any trade-offs or why let's say Java or the old good languages don't have type inference

Java happens to be the exception rather than the rule here. Even C++ (which I beleive qualifies as a "good old language" :) ) supports type inference with the auto keyword since the C++11 standard. It not only works with variable declaration, but also as function return type, which is especially handy with some complex template functions.

Implicit typing and type inference have many good use cases, and there are also some use cases where you really shouldn't do it. This is sometimes matter of taste, and also subject of debate.

But that there are undoubtedly good use cases, is by itself a legitimate reason for any language to implement it. It is also not a hard to implement feature, no runtime penalty, and does not affect compile time significantly.

I do not see a real drawback in giving an opportunity to the developer to use type inference.

Some answerer reasond how explicit typing is good sometimes, and they are certainly right. But not supporting implicit typeing would mean that a language enforces explicit typing all the time.

So the real draw back is when a language does not support implicit typeing, because with this it states, that there is no legitimate reason for the developer to use it, which is not true.

2

The primary distinction between a Hindley-Milner type inference system and Go-style type inference is the direction of information flow. In HM, type information flows forwards and backwards via unification; in Go, type information flows forwards only: it computes forwards substitutions only.

HM type inference is a splendid innovation that works well with polymorphically typed functional languages, but Go's authors would probably argue that it tries to do too much:

  1. The fact that information flows both forwards and backwards means that HM type inference is a very nonlocal affair; in the absence of type annotations, every line of code in your program could be contributing to the typing of a single line of code. If you only have forward substitution, you know that the source of a type error must be in the code that precedes your error; not the code that comes after.

  2. With HM type inference, you tend to think in constraints: when you use a type, you constraint what possible types it can have. At the end of the day, there may be some type variables which are left totally unconstrained. The insight of HM type inference is that, those types really don't matter, and so they are made into polymorphic variables. However, this extra polymorphism can be undesirable for a number of reasons. First, as some people have pointed out, this extra polymorphism could be undesirable: HM concluding a bogus, polymorphic type for some bogus code, and lead to strange errors later. Second, when a type is left polymorphic, this can have consequences for runtime behavior. For example, an overly polymorphic intermediate result is the reason why 'show . read' is considered ambiguous in Haskell; as another example, polymorphic values must be evaluated multiple times for each type they are evaluated at, motivating the monomorphism restriction.

1
  • 2
    Unfortunately, this looks like a good answer to a different question. OP is asking about "Go-style type inference" versus languages that don't have any type inference at all, not "Go-style" versus HM.
    – Ixrec
    Commented Mar 22, 2016 at 9:13
2

Swift does type inference in multiple directions. For example, in "let x = someFunction()" it gets the type of the return value of someFunction() and that's the type of x, but in "let x: String = someFunction()" there may be multiple functions with the same name, and it picks the one returning String. It is useful in other places: If a function has an enum argument of type MyEnum, then instead of MyEnum.someValue I can just write .someValue. There are plenty other shortcuts. If you use a binary operator, and one argument has a type that can be inferred, then that can be used to infer the type of the other argument. Or if the type of the result can be infered, both arguments may be inferred. Like "let x: Double = someFunction() + anotherFunction()". Since the only implementation of "+" giving Double is Double + Double -> Double, someFunction() and anotherFunction() must both be Double.

What helps a lot is that various things are checked. Say someFunction() returns a String or an optional String, and I can't remember and I'm too lazy to look it up. If I write let x: String? = someFunction() then I might have needlessly turned a String into an optional String, so I write let x = someFunction(). Now x has the right type. But what about nil checks? A statement "if x == nil" doesn't compile if x is not optional. And an assignment "let y: String = x" doesn't compile if x is optional. So the compiler will tell you what's the right thing to do. Once you get used to it, it is very effective.

And the downside is of course that you don't know the type of x. But usually you don't care about that. You can pass x to a function that expects the right type. You can call methods of the right class if x is some object. The compiler figures out for you if x is optional or not.

I'd say you get significant savings in code to write, significant savings in code to read, and only in rare case you won't know the type of something.

0

I just decided to "bump" my previous comment into another "answer."

Miguel very-correctly points out that the use of var is quite different from List<BusinessObject> ... and that it omits information which could be very valuable to the subsequent human(!) reader. And, in fact, it presents "quite a quandary" to a poor soul who is right-now just trying to "track down another bug."

• He has to find the definition of someComplexFunction() in order to determine what it returns.

and(!) ...

• He has to consider the non-zero possibility that "the type-inference mechanism, itself," has actually become part of the problem.

You see – at the time the original programmer wrote the original code, "s/he thought that s/he knew what the eventual type would be." But, "what if that is no longer true?" What if subsequent changes to the code, which did not exist at the time, have altered the compiler's decision?

An explicit declaration would have [probably ...] caused any such inadvertent change to result in a compile-time error, which would have come to the attention of this innocent "subsequent programmer" who had no idea that s/he was "creating a bug."


So, I would observe that, in this scenario, type-inference might actually have weakened the compiler's ability to sniff-out programmer mistakes.

It is for this very reason that I typically decline to use this "fee-chur," most but not all of the time. I think that the compiler's "utterly robotic type-checking rigor" is my bestest friend, and I'm quite willing to type a few extra characters to avail myself of it.

-2

It hurts readability.

Compare

var stuff = someComplexFunction()

vs

List<BusinessObject> stuff = someComplexFunction()

It's especially a problem when reading outside an IDE like on github.

4
  • 4
    this doesn't seem to offer anything substantial over points made and explained in prior 6 answers
    – gnat
    Commented Sep 20, 2017 at 4:41
  • If you use proper name instead of "complex unreadable", then var can be used without hurting readability. var objects = GetBusinessObjects();
    – Fabio
    Commented Sep 24, 2017 at 15:15
  • Ok but then you're leaking the type system into the variable names. It's like hungarian notation. After refactorings you're more likely to end up with names that don't match the code. In general, functional style pushes you to lose the natural namespacing advantages that classes provide. So you end up with more squareArea() instead of square.area().
    – miguel
    Commented Sep 25, 2017 at 1:50
  • I understand the comment and basically agree with it. In the example shown, the explicit type declaration informs the human reader what the type is. Otherwise, s/he would have to go to the definition of the function – having first located it – to find out what is returned. [Explicit] Type information is used, not only by the compiler, but by the programmer who's looking for a bug in source code that maybe s/he's never looked at before. Commented Mar 30, 2021 at 13:44

Not the answer you're looking for? Browse other questions tagged or ask your own question.