37

I read code way more often than I write code, and I'm assuming that most of the programmers working on industrial software do this. The advantage of type inference I assume is less verbosity and less written code. But on the other hand if you read code more often, you'll probably want readable code.

The compiler infers the type; there are old algorithms for this. But the real question is why would I, the programmer, want to infer the type of my variables when I read the code? Isn't it more faster for anyone just to read the type than to think what type is there?

Edit: As a conclusion I understand why it is useful. But in the category of language features I see it in a bucket with operator overloading - useful in some cases but affecting readability if abused.

  • 5
    In my experience types are vastly more important when writing code than reading it. When reading code, I'm looking for algorithms and specific blocks which well-named variables will usually direct me to. I really don't need to type check code just to read and understand what it's doing unless it's terribly poorly written. However, when reading code that's bloated with extra unnecessary details that I'm not looking for (like too many type annotations) it makes it often harder to find the bits I am looking for. Type inference I would say is a huge boon for reading far more than writing code. – Jimmy Hoffa Sep 29 '14 at 10:28
  • Once I find the piece of code I'm looking for, then I might start type checking it but at any given time you shouldn't be focused in on more than maybe 10 lines of code, at which point it's no hassle to do the inference yourself because you're mentally picking the whole block apart to begin with, and likely using tooling to help you do so anyway. Figuring out the types of the 10 lines of code you're trying to zone in on rarely takes much of your time at all, but this is the part where you've switched from reading to writing anyway which is so much less common anyway. – Jimmy Hoffa Sep 29 '14 at 10:33
  • Remember that even though a programmer reads code more often than writes it, it does not mean that a piece of code is read more often than written. A lot of code may be short-lived or otherwise never read again, and it may not be easy to tell which code will survive and should be written to maximal readability. – jpa Sep 29 '14 at 11:13
  • 2
    Elaborating on @JimmyHoffa 's first point, consider reading in general. Is a sentence easier to parse and read, let alone understand, when focusing on the part-of-speech of its individual words? "The (article) cow (noun-singular) jumped (verb-past) over (preposition) the (article) moon (noun) . (punctuation-period)". – Zev Spitz Mar 25 '15 at 8:56
46

Let's take a look at Java. Java can't have variables with inferred types. This means I frequently have to spell out the type, even if it is perfectly obvious to a human reader what the type is:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

And sometimes it's just plain annoying to spell out the whole type.

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

This verbose static typing gets in the way of me, the programmer. Most type annotations are repetitive line-filler, content-free regurgiations of what we already know. However, I do like static typing, as it can really help with discovering bugs, so using dynamic typing isn't always a good answer. Type inference is the best of both worlds: I can omit the irrelevant types, but still be sure that my program (type-)checks out.

While type inference is really useful for local variables, it should not be used for public APIs which have to be unambiguously documented. And sometimes the types really are critical for understanding what's going on in the code. In such cases, it would be foolish to rely on type inference alone.

There are many languages that support type inference. For example:

  • C++. The auto keyword triggers type inference. Without it, spelling out the types for lambdas or for entries in containers would be hell.

  • C#. You can declare variables with var, which triggers a limited form of type inference. It still manages most cases where you want type inference. In certain places you can leave out the type completely (e.g. in lambdas).

  • Haskell, and any language in the ML family. While the specific flavour of type inference used here is quite powerful, you still often see type annotations for functions, and for two reasons: The first is documentation, and the second is a check that type inference actually found the types you expected. If there is a discrepancy, there's likely some kind of bug.

  • 13
    Note also that C# has anonymous types, i.e. types without a name, but C# has a nominal type system, i.e. a type system based on names. Without type inference, those types could never be used! – Jörg W Mittag Sep 28 '14 at 14:52
  • 10
    Some examples are a bit contrived, in my opinion. Initialization to 42 doesn't automatically mean that the variable is an int, it could be any numeric type including even char. Also I don't see why you would want to spell out the whole type for the Entry when you can just type in the class name and let your IDE do the necessary import. The only case when you have to spell out the whole name is when you have a class with the same name in your own package. But it seems to me like bad design anyway. – Malcolm Sep 28 '14 at 16:20
  • 10
    @Malcolm Yes, all my examples are contrived. They serve to illustrate a point. When I wrote the int example, I was thinking about the (in my opinion pretty sane behaviour) of most languages that feature type inference. They usually infer an int or Integer or whatever it's called in that language. The beauty of type inference is that it's always optional; you can still specify a different type if you need one. Regarding the Entry example: good point, I'll replace it with Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. Java doesn't even have type aliases :( – amon Sep 28 '14 at 17:01
  • 4
    @m3th0dman If the type is important for understanding, then you can still explicitly mention it. Type inference is always optional. But here, the type of colKey is both obvious and irrelevant: we only care about it being suitable as the second argument of doSomethingWith. If I were to extract that loop into a function that yields an Iterable of (key1, key2, value)-triples, the most general signature would be <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). Inside that function, the real type of colKey (Integer, not K2) is absolutely irrelevant. – amon Sep 29 '14 at 7:52
  • 4
    @m3th0dman it's quite a broad sweeping statement, about "most" code being this or that. Anecdotal stats. There's certainly no point in writing the type twice in initializers: View.OnClickListener listener = new View.OnClickListener(). You'd still know the type even if the programmer was "lazy" and shortened it to var listener = new View.OnClickListener (if this was possible). This sort of redundancy is common - I will not risk a guesstimate here - and removing it does stem from thinking about future readers. Every language feature should be used with care, I'm not questioning that. – Konrad Morawski Sep 29 '14 at 13:10
26

It's true that code is read far more often than it is written. However, reading also takes time, and two screens of code are harder to navigate and read than one screen of code, so we need to prioritize to pack the best useful-information/reading-effort ratio. This is a general UX principle: Too much information at once overwhelms and actually degrades the effectiveness of the interface.

And it is my experience that often, the exact type isn't (that) important. Surely you sometimes nest expressions: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Each of these contains sub-expressions that evaluate to a value whose type is not written out. Yet they are perfectly clear. We're okay with leaving the type unstated because it's easy enough to figure out from the context, and writing it out would do more harm than good (clutter the understanding of the data flow, take valuable screen and short-term memory space).

One reason to not nest expressions like there's no tomorrow is that lines get long and the flow of values becomes unclear. Introducing a temporary variable helps with this, it imposes an order and gives a name to a partial result. However, not everything that benefits from these aspects also benefits from having its type spelled out:

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

Does it matter whether user is an entity object, an integer, a string, or something else? For most purposes, it does not, it's enough to know that it represents a user, comes from the HTTP request, and is used to fetch the name to display in the lower right corner of the answer.

And when it does matter, the author is free to write out the type. This is a freedom that must be used responsibly, but the same is true for everything else that can enhance readability (variable and function names, formatting, API design, white space). And indeed, the convention in Haskell and ML (where everything can be inferred without extra effort) is to write out the types of non-local functions functions, and also of local variables and functions whenever appropriate. Only novices let every type be inferred.

  • 2
    +1 This should be the accepted answer. It goes right to the heart of why type inference is a great idea. – Christian Hayter Sep 29 '14 at 15:13
  • The exact type of user does matter if you are trying to extend the function, because it determines what you can do with the user. This is important if you want to add some sanity check (due to a security vulnerability, for example), or forgot that you actually need to do something with the user in addition to just displaying it. True, these kinds of reading for expansion are less often than just reading the code, but they also are a vital part of our job. – cmaster Aug 18 '16 at 6:59
  • @cmaster And you can always find out that type rather easily (most IDEs will tell you, and there's the low-tech solution of intentionally causing a type error and letting the compiler print the actual type), it's just out of the way so it doesn't annoy you in the common case. – user7043 Aug 18 '16 at 8:33
4

I think type inference is quite important and should be supported in any modern language. We all develop in IDEs and they could help a lot in case you want to know the inferred type, just few of us hack in vi. Think of verbosity and ceremony code in Java for instance.

  Map<String,HashMap<String,String>> map = getMap();

But you can say it's fine my IDE will help me, it could be a valid point. However, some features wouldn't be there without the help of type inference, C# anonymous types for instance.

 var person = new {Name="John Smith", Age = 105};

Linq wouldn't be as nice as it is now without the help of type inference, Select for instance

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

This anonymous type will be inferred neatly to the variable.

I dislike type inference on return types in Scala because I think your point applies here, it should be clear for us what a function returns so we can use the API more fluently

  • Map<String,HashMap<String,String>>? Sure, if you're not using types, then spelling them out has little benefit. Table<User, File, String> is more informative though, and there is benefit in writing it. – MikeFHay Sep 29 '14 at 21:00
4

I think the answer to this is really simple: it saves reading and writing redundant information. Particularly in object oriented languages where you have a type on both sides of the equal sign.

Which also tells you when you should or should not use it -- when the information isn't redundant.

  • 3
    Well, technically, the information is always redundant when it's possible to omit manual signatures: otherwise the compiler wouldn't be able to infer them! But I get what you mean: when you just duplicate a signature to multiple spots in a single view, it's really redundant to the brain, while a few well-placed type give information you'd have to search a long while, possibly with a good many nonobvious transformations. – leftaroundabout Sep 28 '14 at 22:37
  • @leftaroundabout: redundant when read by the programmer. – jmoreno Sep 29 '14 at 4:13
3

Suppose one sees the code:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

If someBigLongGenericType is assignable from the return type of someFactoryMethod, how likely would someone reading the code be to notice if the types don't match precisely, and how readily could someone who did notice the discrepancy recognize whether it was intentional or not?

By allowing inference, a language can suggest to someone who is reading code that when the type of a variable is explicitly stated the person should try to find a a reason for it. This in turn allows people who are reading code to better focus their efforts. If, by contrast, the vast majority of the times when a type is specified, it happens to be exactly the same as what would have been inferred, then someone who is reading code may be less prone to notice the times that it is subtly different.

2

I see that there are a number of fine answers already. Some of which I will be repeating but sometimes you just want to put things in your own words. I will comment with some examples from C++ because that is the language with which I have the most familiarity.

What is necessary is never unwise. Type inference is necessary to make other language features practical. In C++ it is possible to have unutterable types.

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C++11 added lambdas which are also unutterable.

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

Type inference also underpins templates.

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

But your questions were "why would I, the programmer, want to infer the type of my variables when I read the code? Isn't it more faster for anyone just to read the type than to think what type is there?"

Type inference removes redundancy. When it comes to reading code it may sometimes be faster and easier to to have redundant information in the code but redundancy can overshadow the useful information. For example:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

It does not take much familiarity with the standard library for a C++ programmer to identify that i is an iterator from i = v.begin() so the explicit type declaration is of limited value. By its presence it obscures details that are more important (such as that i points to the beginning of the vector). The fine answer by @amon provides an even better example of verbosity overshadowing important details. In contrast using type inference gives greater prominence to the important details.

std::vector<int> v;
auto i = v.begin();

While reading code is important it is not sufficient, at some point you will have to stop reading and start writing new code. Redundancy in code makes modifying code slower and harder. For example, say I have the following fragment of code:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

In the case that I need to change the value type of the vector to double changing the code to:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

In this case I have to modify the code in two places. Contrast with type inference where the original code is:

std::vector<int> v;
auto i = v.begin();

And the modified code:

std::vector<double> v;
auto i = v.begin();

Note that I now only have to change one line of code. Extrapolate this to a large program and type inference can propagate changes to types much more quickly than you can with an editor.

Redundancy in code creates the possibility of bugs. Any time your code is dependent on two pieces of information being kept equivalent there is a possibility of mistake. For example, there is an inconsistency between the two types in this statement which is probably not intended:

int pi = 3.14159;

Redundancy makes intention harder to discern. In some cases type inference can be easier to read and understand because it is simpler than explicit type specification. Consider the fragment of code:

int y = sq(x);

In the case that sq(x) returns an int, it is not obvious whether y is an int because it is the return type of sq(x) or because it suits the statements that use y. If I change other code such that sq(x) no longer returns int, it is uncertain from that line alone whether the type of y should be updated. Contrast with the same code but using type inference:

auto y = sq(x);

In this the intent is clear, y must be the same type as returned by sq(x). When the code changes the return type of sq(x), the type of y changes to match automatically.

In C++ there is a second reason why the above example is simpler with type inference, type inference can not introduce implicit type conversion. If the return type of sq(x) is not int, the compiler with silently insert an implicit conversion to int. If the return type of sq(x) is a type complex type which defines operator int(), this hidden function call may be arbitrarily complex.

  • Quite a good point about the unutterable types in C++. However, I would think that's less of a reason to add type inference, than it is a reason to fix the language. In the first case you present, the programmer would just have to give the thing a name to avoid using type inference, so this is not a strong example. The second example is only strong because C++ explicitly forbids lambda types being utterable, even typedefing with use of typeof is rendered useless by the language. And that is a deficit of the language itself that should be fixed in my opinion. – cmaster Aug 18 '16 at 7:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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