Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ambiguous type resolution in typeclass default method instantiations

Why exactly does the following code fail to type?

{-# LANGUAGE AllowAmbiguousTypes, MultiParamTypeClasses #-}

module Main where

class Interface a b c where
  get :: a -> [b]
  change :: b -> c

  changeAll :: a -> [c]
  changeAll = map change . get

main = return ()

If I comment out the default instantiation for --changeAll = map change . get, everything seems fine. However, with the instantiation in place, I get this error:

GHCi, version 8.6.5: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( test.hs, interpreted )

test.hs:10:19: error:
    • Could not deduce (Interface a0 b0 c)
        arising from a use of ‘change’
      from the context: Interface a b c
        bound by the class declaration for ‘Interface’ at test.hs:5:7-15
      The type variables ‘a0’, ‘b0’ are ambiguous
      Relevant bindings include
        changeAll :: a -> [c] (bound at test.hs:10:3)
    • In the first argument of ‘map’, namely ‘change’
      In the first argument of ‘(.)’, namely ‘map change’
      In the expression: map change . get
   |
10 |   changeAll = map change . get
   |                   ^^^^^^

test.hs:10:28: error:
    • Could not deduce (Interface a b0 c0) arising from a use of ‘get’
      from the context: Interface a b c
        bound by the class declaration for ‘Interface’ at test.hs:5:7-15
      The type variables ‘b0’, ‘c0’ are ambiguous
      Relevant bindings include
        changeAll :: a -> [c] (bound at test.hs:10:3)
    • In the second argument of ‘(.)’, namely ‘get’
      In the expression: map change . get
      In an equation for ‘changeAll’: changeAll = map change . get
   |
10 |   changeAll = map change . get
   |                            ^^^

Am I missing something obvious here?

like image 983
andrew-wja Avatar asked Dec 19 '25 21:12

andrew-wja


1 Answers

All your methods are ambiguously typed.

To illustrate the problem better, let's reduce the example to just one method:

class C a b c where
    get :: a -> [b]

Now imagine you have the following instances:

instance C Int String Bool where
    get x = [show x]

instance C Int String Char where
    get x = ["foo"]

And then imagine you're trying to call the method:

s :: [String]
s = get (42 :: Int)

From the signature of s, the compiler knows that b ~ String. From the parameter of get, the compiler knows that a ~ Int. But what is c? The compiler doesn't know. There is nowhere to glean that from.

But wait! Both instances of C match a ~ Int and b ~ String, so which to choose? Not clear. Not enough information. Ambiguous.

This is exactly what happens when you're trying to call get and change in map change . get: there is not enough type information for the compiler to understand what a, b, and c are for either the get call or the change call. Oh, and keep in mind: both those calls may be coming from different instances. There is nothing to say that they must be from the same instance as changeAll itself.


There are two possible ways to fix this.

First, you may use a functional dependency, which is a way to say that in order to determine c, it should be enough to know a and b:

class C a b c | a b -> c where ...

If you declare the class this way, the compiler will reject multiple instances for same a and b, but different c, and on the flip side, it will be able to pick the instance just by knowing a and b.

Of course, you can have multiple functional dependencies on the same class. For example, you may declare that knowing any two variables should be enough to determine the third one:

class C a b c | a b -> c, a c -> b, b c -> a where ...

Keep in mind that for your changeAll function even these three functional dependencies won't be enough, because the implementation of changeAll "swallows" b. That is, when it calls get, the only type known is a. And similarly, when it calls change, the only type known is c. This means that, in order for such "swallowing" of b to work, it must be determinable by a alone, as well as by c alone:

class Interface a b c | a -> b, c -> b where ...

Of course, this will only be possible if the logic of your program really does have this property that some variables are determined by others. If you really need all variables to be independent, read on.


Second, you can explicitly tell the compiler what the types must be by using TypeApplications:

 s :: String
 s = get @Int @String @Bool 42  -- works

No more ambiguity. The compiler knows exactly which instance to pick, because you have told it explicitly.

Applying this to your changeAll implementation:

changeAll :: a -> [c]
changeAll = map (change @a @b @c) . get @a @b @c

(NOTE: in order to be able to reference type variables a, b, and c in the function body like that, you also need to enable ScopedTypeVariables)

And of course you'll also need to do this when calling changeAll itself, since it also doesn't have enough information in its type signature:

foo = changeAll @Int @String @Bool 42
like image 54
Fyodor Soikin Avatar answered Dec 21 '25 19:12

Fyodor Soikin