7
votes

Consider the function,

add a b = a + b

This works:

*Main> add 1 2
3

However, if I add a type signature specifying that I want to add things of the same type:

add :: a -> a -> a
add a b = a + b

I get an error:

test.hs:3:10:
    Could not deduce (Num a) from the context ()
      arising from a use of `+' at test.hs:3:10-14
    Possible fix:
      add (Num a) to the context of the type signature for `add'
    In the expression: a + b
    In the definition of `add': add a b = a + b

So GHC clearly can deduce that I need the Num type constraint, since it just told me:

add :: Num a => a -> a -> a
add a b = a + b

Works.

Why does GHC require me to add the type constraint? If I'm doing generic programming, why can't it just work for anything that knows how to use the + operator?

In C++ template programming, you can do this easily:

#include <string>
#include <cstdio>

using namespace std;

template<typename T>
T add(T a, T b) { return a + b; }

int main()
{
    printf("%d, %f, %s\n",
           add(1, 2),
           add(1.0, 3.4),
           add(string("foo"), string("bar")).c_str());
    return 0;
}

The compiler figures out the types of the arguments to add and generates a version of the function for that type. There seems to be a fundamental difference in Haskell's approach, can you describe it, and discuss the trade-offs? It seems to me like it would be resolved if GHC simply filled in the type constraint for me, since it obviously decided it was needed. Still, why the type constraint at all? Why not just compile successfully as long as the function is only used in a valid context where the arguments are in Num?

4
Why should it magically turn the type signature into something much less general by adding a constraint when you explicitly told it you want no constraints (by declaring add :: a -> a -> a)? Also note that in Haskell, there is no thing that has (+) overloaded but isn't an instance of Num (because (+) is in Num, so to overload it you have to declare a Num instance).user395760

4 Answers

15
votes

If you don't want to specify the function's type, just leave it out and the compiler will infer the types automatically. But if you choose to specify the types, they have to be correct and accurate.

15
votes

The entire point of types is to have a formal way to declare the right and wrong way to use a function. A type of (Num a) => a -> a -> a describes exactly what is required of the arguments. If you omitted the class constraint, you would have a more general function that could be used (erroneously) in more places.

And it’s not just preventing you from passing non-Num values to add. Everywhere the function goes, the type is sure to go. Consider this:

add :: a -> a -> a
add a b = a + b
foo :: [a -> a -> a]
foo = [add]
value :: [String]
value = [f "hello" "world" | f <- foo]

You want the compiler to reject this, right? How does it do that? By adding class constraints, and checking that they are not removed, even if you don’t directly name the function.

What’s different in the C++ version? There are no class constraints. The compiler substitutes int or std::string for T, then tries to compile the resulting code and looks for a matching + operator that it can use. The template system is “looser”, since it accepts more invalid programs, and this is a symptom of it being a separate stage before compilation. I would love to modify C++ to add the <? extends T> semantics from Java’s generics. Just learn the type system and recognize that parametric polymorphism is “stronger” than C++ templates, namely it will reject more invalid programs.

12
votes

I think you might be being tripped up by the "crazy moon poetry" of GHC's error messages. It's not saying that it (being GHC) couldn't deduce the (Num a) constraint. It is saying that the (Num a) constraint can't be deduced from your type signature, which it knows must be there from the use of +. Hence, you're are stating that this function has a type more general than the compiler knows it can have. The compiler doesn't want you lying about your functions to the world!

In the first example you gave, without the type signature, if you run :t add in ghci, you'll see that the compiler knows full well that the (Num a) constraint is there.

As for C++'s templates, remember that they are syntactic templates and are only fully type checked in each instance as they are used. Your add template will work with any types so long as, at each place it is used, there is a suitable + operator and perhaps conversions, to make an instance of the template viable. No guarantees can be made about the template until then... which is why the body of the template must be "visible" to each module that uses it.

Basically, all C++ can do is validate the syntax of the template, and then keep it around as a kind of very hygienic macro. Whereas Haskell generates a real function for add (leaving aside that it may choose to also generate type specific specializations for optimization).

2
votes

There are cases where the compiler can't figure out the right type for you and where it needs your help. Consider

f s = show $ read s

The compiler says:

Ambiguous type variable `a' in the constraints:
Read a' arising from a use of `read' at src\Main.hs:20:13-18
`Show a' arising from a use of `show' at src\Main.hs:20:6-9
Probable fix: add a type signature that fixes these type variable(s)

(Strange enough, it seems you can define this function in ghci, but it seems there is no way to actually use it)

If you want that something like f "1" works, you need to specify the type:

f s = show $ (read s :: Int)