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
)
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
.
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.
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