Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell ad hoc polymorphism

I'm trying to get my head around ad-hoc polymorphism in haskell, that is having the same function provide different behaviour for different argument types.

But while the following test code compiles

{-# LANGUAGE MultiParamTypeClasses #-}

class MyClass a b where
    foo :: a -> b

instance MyClass Bool Int where
    foo True = 0
    foo False = 1

instance MyClass Double Double where
    foo x = -x

if I try to call it using something like

foo True

ghci yells at me:

No instance for (MyClass Bool b0) arising from a use of `foo'
The type variable `b0' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there is a potential instance available:
  instance MyClass Bool Int -- Defined at test.hs:6:10
Possible fix: add an instance declaration for (MyClass Bool b0)
In the expression: foo True
In an equation for `it': it = foo True

However, if I specify the return type, it works:

foo True :: Int -- gives 0

Why is this needed? The parameter type of Bool should be enough to resolve the ambiguity.

Also: Is this the "best" way to achieve similar behaviour? (without renaming the functions to fooBool and fooDouble)

like image 403
wonce Avatar asked May 04 '15 20:05

wonce


2 Answers

The problem you're facing is that the overloading is determined by all of the types in the class—including ones that only appear as return types. You could have instances for both MyClass Bool Int and MyClass Bool String, and it would be able to disambiguate based on what type is expected.

One of the core design tradeoffs with Haskell typeclasses is the "open world assumption". Haskell type instances are implicitly global: a specific type (or sequence of types, in this case) can only have one instance in the whole program, which is implicitly exported to all the modules using that type.

This makes it really easy to get new instances of some class without realizing it, so the Haskell typechecker assumes that instances could possibly exist for any valid combination of types. In your case, this means that while MyClass Bool Int is the only instance using Bool, it's still ambiguous with other possible MyClass Bool b instances.

Once you add an annotation for the type of the whole expression, it stops being ambiguous because both a and b are fixed.

To get the behavior you expect, you can use FunctionalDependencies. These allow you to specify that there is only one possible b for any given a, which will let GHC infer the type correctly. It would look something like this:

class MyClass a b | a -> b where

Of course, this does intentionally throw out some flexibility: now you can't have instances for both MyClass Bool Int and MyClass Bool String.

like image 150
Tikhon Jelvis Avatar answered Sep 25 '22 15:09

Tikhon Jelvis


Tikhon Jelvis elaborated the problem and proposed to use functional dependencies, but there is an alternative: type families. Your code becomes

{-# LANGUAGE TypeFamilies #-}

class MyClass a where
    type R a
    foo :: a -> R a

instance MyClass Bool where
    type R Bool = Int
    foo True  = 0
    foo False = 1

instance MyClass Double where
    type R Double = Double
    foo x = -x

We use associated type synonyms here. I like these explicit type level functions, but if you don't, it's OK to use fundeps, because differences are rather subtle.

like image 39
user3237465 Avatar answered Sep 25 '22 15:09

user3237465