Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MultiParamTypeClasses - Why is this type variable ambiguous?

Suppose I define a multi-parameter type class:

{-# LANGUAGE MultiParamTypeClasses, AllowAmbiguousTypes, FlexibleContexts, FlexibleInstances #-}

class Table a b c where
  decrement :: a -> a
  evalutate :: a -> b -> c

Then I define a function that uses decrement, for simplicity:

d = decrement

When I try to load this in ghci (version 8.6.3):

• Could not deduce (Table a b0 c0)
    arising from a use of ‘decrement’
  from the context: Table a b c
    bound by the type signature for:
               d :: forall a b c. Table a b c => a -> a
    at Thing.hs:13:1-28
  The type variables ‘b0’, ‘c0’ are ambiguous
  Relevant bindings include d :: a -> a (bound at Thing.hs:14:1)
  These potential instance exist:
    instance Table (DummyTable a b) a b

This is confusing to me because the type of d is exactly the type of decrement, which is denoted in the class declaration.

I thought of the following workaround:

data Table a b = Table (a -> b) ((Table a b) -> (Table a b))

But this seems notationally inconvenient, and I also just wanted to know why I was getting this error message in the first place.

like image 473
Eben Kadile Avatar asked Jun 07 '19 02:06

Eben Kadile


1 Answers

The problem is that, since decrement only requires the a type, there is no way to figure out which types b and c should be, even at the point where the function is called (thus solving the polymorphism into a specific type) - therefore, GHC would be unable to decide which instance to use.

For example: let's suppose you have two instances of Table: Table Int String Bool, and Table Int Bool Float; you call your function d in a context where it is supposed to map an Int to another Int - problem is, that matches both instances! (a is Int for both).

Notice how, if you make your function equal to evalutate:

d = evalutate

then the compiler accepts it. This is because, since evalutate depends on the three type parameters a, b, and c, the context at the call site would allow for non-ambiguous instance resolution - just check which are the types for a, b, and c at the place where it is called.

This is, of course, not usually a problem for single-parameter type classes - only one type to resolve; it is when we deal with multiple parameters that things get complicated...

One common solution is to use functional dependencies - make b and c depend on a:

 class Table a b c | a -> b c where
  decrement :: a -> a
  evalutate :: a -> b -> c

This tells the compiler that, for every instance of Table for a given type a, there will be one, and only one, instance (b and c will be uniquely determined by a); so it will know that there won't be any ambiguities and accept your d = decrement happily.

like image 161
typedfern Avatar answered Nov 02 '22 16:11

typedfern