trpnd
trpnd

Reputation: 474

How to provide an overlapping typeclass instance for an instantiation of a generic instance?

I'd like to provide a generic implementation of a multi-parameter typeclass, then provide overlapping instances for specific instantiations of said typeclass. This works with a single-parameter class, but fails to compile once I add type parameters. Below is a toy example of the kind of instances I'm trying to write:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}

class Foo a b where
  foo :: a -> b -> String

instance {-# OVERLAPPABLE #-} Foo a b where 
  foo _ _ = "foo"

instance Foo Int String where
  foo _ y = y

mapFoo :: a -> [b] -> [String]
mapFoo x ys = map (foo x) ys

This example fails to compile due to an error at the call site:

    • Overlapping instances for Foo a b arising from a use of ‘foo’
      Matching instances:
        instance [overlappable] Foo a b -- Defined at overlapping.hs:7:31
        instance Foo Int String -- Defined at overlapping.hs:10:10
      (The choice depends on the instantiation of ‘a, b’
       To pick the first instance above, use IncoherentInstances
       when compiling the other instance declarations)
    • In the first argument of ‘map’, namely ‘(foo x)’
      In the expression: map (foo x) ys
      In an equation for ‘mapFoo’: mapFoo x ys = map (foo x) ys
   |
14 | mapFoo x ys = map (foo x) ys

I've tried adding various pragma annotations, as well as explicit type applications but haven't been able to get this to compile. Is there a way to do so? Or is there a better way to express the same kind of behavior?

Upvotes: 0

Views: 94

Answers (1)

Fyodor Soikin
Fyodor Soikin

Reputation: 80744

It's not the instances themselves, it's the function. In order to use foo, mapFoo must have a constraint, which would require the caller to provide an instance of the class:

mapFoo :: Foo a b => a -> [b] -> [String]

If you meant to express that mapFoo should always use the "base" instance because a and b are unknown here, then just use it directly:

mapFoo x ys = map (const "foo") ys

And if you meant for the compiler to magically choose an instance depending on how a and b get instantiated later, then this cannot happen. At the time mapFoo is compiled, it's not yet known what a and b are, and therefore which implementation of foo to use. It must be able to either choose an implementation locally based on locally-known types or get it as a parameter from the caller.

Upvotes: 3

Related Questions