MultiParamTypeClasses - Why is this type variable ambiguous?
Asked Answered
E

1

5

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.

Esparza answered 7/6, 2019 at 2:49 Comment(3)
This is an example of the harm GHC's error messages are causing. Your original class Table would not compile. GHC issued a message saying AllowAmbiguousTypes might help. So I see you switched it on. Terrible idea, and GHC should never have mentioned it. Functional Dependencies, per @typedfern's answer is a much better approach. Grr. I am trying to fix GHC to stop making ridiculous suggestions -- esp for newbies.Wellbalanced
@Wellbalanced I agree and disagree. In general, AllowAmbiguousTypes is perfectly fine if you are then prepared to use TypeApplications to disambiguate. I prefer that to using proxies. OTOH, in this specific case, it is not the right solution, and the GHC suggestion is not appropriate. Here, fundeps (or type families) should be used.Intercontinental
People who know what they're doing/plan to use TypeApplications probably have AllowAmbiguousTypes switched on. So the message should be appropriate for people who make a mistake in their method sigs. It should mention all the possible approaches/corrections, not favour an advanced feature (which it doesn't actually name) and keep otherwise silent. This is the type astronauts taking over the hen-house.Wellbalanced
C
8

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.

Colman answered 7/6, 2019 at 3:15 Comment(1)
If you can't use FunctionalDependencies (because you need to support two different Table a b c for the same a and different b and/or c), you can split decrement off into a separate class, and have class Decrement a => Table a b c where evaluate :: a -> b -> c. This means every Table sharing an a must decrement its as the same way, but they can still evaluate differently, unlike class Table a b c | a -> b c where if two Table instances share an a they must actually be the same instance.Pines

© 2022 - 2024 — McMap. All rights reserved.