I'm curious as to how often experienced Haskell programmers really use type inference in practice. I often see it praised as an advantage over the always-explicit declarations needed in certain other languages, but for some reason (perhaps just because I'm new) it "feels" right to write a type signature just about all the time... and I'm sure in some cases it really is required.
Can some experienced Haskellers (Haskellites? Haskellizers?) provide some input?
It's still an advantage, even if you write type signatures, because the compiler will catch type errors in your functions. I usually write type signatures too, but omit them in places like where
or let
clauses where you actually define new symbols but don't feel the need to specify a type signature.
Stupid example with a strange way to calculate squares of numbers:
squares :: [Int]
squares = sums 0 odds
where
odds = filter odd [1..]
sums s (a:as) = s : sums (s+a) as
square :: Int -> Int
square n = squares !! n
odds
and sums
are functions that would need a type signature if the compiler wouldn't infer them automatically.
Also if you use generic functions, like you usually do, type inference is what ensures that you really combine all those generic functions together in a valid way. If you, in the above example, say
squares :: [a]
squares = ...
The compiler can deduce that this isn't valid this way, because one of the used functions (the odd
function from the standard library), needs a
to be in the type class Integral
. In other languages you usually only recognize this at a later point.
If you write this as a template in C++, you get a compiler error when you use the function on a non-Integral type, but not when you define the template. This can be quite confusing, because it's not immediately clear where you've gone wrong and you might have to look through a long chain of error messages to find the real source of the problem. And in something like python you get the error at runtime at some unexpected point, because something didn't have the expected member functions. And in even more loosely typed languages you might not get any error, but just unexpected results.
In Haskell the compiler can ensure that the function can be called with all the types specified in it's signature, even if it's a generic function that is valid for all types that fulfill some constrains (aka type classes). This makes it easy to program in a generic way and use generic libraries, something much harder to get right in other languages. Even if you specify a generic type signature, there is still a lot of type inference going on in the compiler to find out what specific type is used in each call and if this type fulfills all the requirements of the function.
I always write the type signature for top-level functions and values, but not for stuff in "where", "let" or "do" clauses.
First, top level functions are generally exported, and Haddock needs a type declaration to generate the documentation.
Second, when you make a mistake the compiler errors are a lot easier to decode if the compiler has type information available. In fact sometimes in a complicated "where" clause I get an incomprehensible type error so I add temporary type declarations to find the problem, a bit like the type-level equivalent of printf debugging.
So to answer the original question, I use type inference a lot but not 100% of the time.
You have good instincts. Because they are checked by the compiler, type signatures for top-level values provide invaluable documentation.
Like others, I almost always put a type signature for a top-level function, and almost never for any other declaration.
The other place type inference is invaluable is at the interactive loop (e.g., with GHCi). This technique is most helpful when I'm designing and debugging some fancy new higher-order function or some such.
When you are faced with a type-check error, although the Haskell compiler does provide information on the error, this information can be hard to decode. To make it easier, you can comment out the function's type signature and then see what the compiler has inferred about the type and see how it differs from your intended type.
Another use is when you are constructing an 'inner function' inside a top level function but you are not sure how to build the inner function or even what its type should be. What you can do is to pass the inner-function in as an argument to the top level function and then ask ghci for the type of the type level function. This will include the type of the inner function. You can then use a tool like Hoogle to see if this function already exists in a library.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With