17

I am trying to code using 'good Haskell style' and so am trying to follow typical coding standards I find around. Also, compiling with -Wall and -Werror as I am used to when coming from C. One of the warnings I frequently get is "Top level binding with no type signature" and then the compiler tells me what the type signature should be.

I am missing what the advantage of having the type signature explicitly defined is. As a concrete example:

-- matchStr :: String -> String ->  Maybe (String)
matchStr str s
        | isPrefixOf str s = Just(drop (length str) s)
        | otherwise = Nothing

Now what happens if I want to change the type from String to ByteString to improve performance; I'll have to import the ByteString package and use a qualified version of some functions. No other changes are necessary. If I have the type signature then I also have to change this and yet the Haskell compiler would notice this change and correctly infer the new types.

So what am I missing? Why is it considered a good idea to explicitly put type signatures on functions in the general case? i.e. I understand that there might be exceptions where it is a good idea, but why is it considered good in general?

dave
  • 4,812
  • 4
  • 25
  • 38
  • As for refactoring to use other types: that's generally _easier_ with signatures; you can do lots of things with polymorphic functions which will work on _any_ suitable type, and then you can simply select that type in the signature (possibly even in a central `type StringType = ByteString` without needing to touch much of the implementations. And the compiler will be much more helpful with the implementations than without signatures. – leftaroundabout May 01 '14 at 08:55

4 Answers4

45

If you make a mistake in defining your function, the compiler might infer a type that isn't what you expected it to be. If you've declared the type you expect, the compiler will report the error in the function's definition.

Without the declaration, the compiler has no way to know that its inferred type is "wrong", and it will instead end up reporting errors in the places where you try to call the function, which makes it less clear where the problem really lies.

If the calling functions don't have type declarations either, then instead of reporting errors there, the compiler might just infer incorrect types for those too, causing problems in their callers. You'll end up getting an error message somewhere, but it may be quite far removed from the actual root of the problem.


Also, you can declare a more specific type than what the compiler would infer. For example, if you write the function:

foo n = n + 1

The compiler will infer the type Num a => a -> a, which means it must compile generic code that can work with any Num instance. If you declare the type as Int -> Int, the compiler may be able to produce more efficient code that's specialized for integers only.


Finally, type declarations serve as documentation. The compiler may be able to infer the types of complex expressions, but it's not so easy for a human reader. A type declaration provides the "big picture" that can help a programmer understand what the function does.

Note that Haddock comments are attached to declarations, not definitions. Writing a type declaration is the first step toward providing additional documentation for a function using Haddock.

Wyzard
  • 33,849
  • 3
  • 67
  • 87
  • Well-stated and thorough explanation. +1 – ApproachingDarknessFish May 01 '14 at 16:55
  • Infering generic types at top level requires `NoMonomorphismRestriction`, otherwise they're inferred as the most specific possible type. – Ilmo Euro May 28 '14 at 20:01
  • 1
    @IlmoEuro That is misleading. *Variable-like* definitions get inferred with monomorphic types, but function-like ones don't. `foo = \x -> x + 1; bar = 5` both get monomorphized because they don't "look like" functions (they don't bind arguments on the LHS). `foo x = x + 1` does not get monomorphized no matter the state of `-XMonomorphismRestriction`. – HTNW Feb 28 '18 at 14:54
22

I would consider documentation one advantage of having an explicit type signature.

From "Types and Programming Languages":

Types are also useful when reading programs. The type declarations in procedure headers and module interfaces constitute a form of documentation, giving useful hints about behavior. Moreover, unlike descriptions embedded in comments, this form of documentation cannot become outdated, since it is checked during every run of the compiler. This role of types is particularly important in module signatures.

Chris Wesseling
  • 6,226
  • 2
  • 36
  • 72
creichert
  • 538
  • 1
  • 3
  • 7
  • 2
    This aspect is especially useful when the use of first class functions means your function definition might not appear to have the same number of parameters as its type would indicate. – Ben May 01 '14 at 07:16
15

There are several reasons.

  1. It may make the code easier for humans to read. (OTOH, if you have dozens of tiny definitions, sometimes the type signatures add more clutter.)
  2. If your implementation is wrong, the compiler may infer the wrong type. This may cause other functions' types to be inferred wrong, finally resulting in a type error very far away from the actual broken function.
  3. You may want to give a function a less polymorphic type than it could have, for performance reasons.
  4. You may want to use type aliases. This allows you to quickly change a type in several places, and also to document some of the intent behind a value. (Compare FilePath vs String.)
  5. The compiler can figure out types automatically, but not all external tools can do this. (E.g., originally Haddock would refuse to produce documentation for functions lacking an explicit type signature - although I gather this is fixed now.)

It is worth noting that some people advocate that you start with the type signatures, and fill in the implementations later.

At any rate, most people seem to recommend type signatures for all or most top-level declarations. Whether you give them for local variables / functions is a matter of taste.

MathematicalOrchid
  • 61,854
  • 19
  • 123
  • 220
  • 1
    And don't forget the dreaded `MonomorphismRestriction`, where Haskell may actually restrict the type beyond normal type inference, unless you give an explicit type signature. The toplevel definition `answer = 42` will have an inferred type of `Integer`, rather than the more general `Num a => a`. –  May 01 '14 at 12:46
  • 1
    Well, yes, there are a couple of instances where you _must_ provide a type signature (e.g., ambiguous types). The question, I think, was about when the types are purely optional. – MathematicalOrchid May 01 '14 at 12:51
1

Your contrived example is really contrived, since the function body does not depend on the type of contents of the list. In this case it is indeed difficult to see what's the benefit of defining the type to be [String] -> ([String],[String]) instead of [a]->([a],[a])

If you attempt to define a function that depends on the contents, you will see that the type definition is not the only thing you need to change. For example, changing a list for MArray is going to be far more involved, not just using a function that happens to have the same name in a different module. So qualifying the name during refactoring in a few narrow cases is not a good enough reason to not specify type signatures.

Specifying the type tells the compiler a little bit of the intent. Then the compiler will be able to report the mismatch of the intent and the implementation.

Sassa NF
  • 5,306
  • 15
  • 22
  • Okay, I've updated my example (sure it could still be [a] -> [a] -> Maybe([a]) but then changing it to ByteStrings definitely changes it from a to ByteString due to using qualified functions .... does this change anything?) – dave May 01 '14 at 10:32
  • @dave maybe i am missing your point, but it seems to me you refer to simplicity of refactoring as the reason to not specify a signature. In return I am trying to say that such simple refactoring may turn out to be not a common case. – Sassa NF May 01 '14 at 11:04