Search code examples
haskelltypeclassexistential-type

Instance for existential wrapper with a variable in contravariant position


I have the following definition:

{-# LANGUAGE ExistentialQuantification #-}

module Test
where

class Named a where
  name :: a -> String

data Wrap = forall a . (Named a, Read a) => Wrap (a -> IO ())

I want to write Named instance for Wrap. The next doesn't work:

instance Named Wrap where
  name (Wrap named) =
    let a = undefined
        _ = named a
    in name a

The error:

Could not deduce (Named a0) arising from a use of ‘name’
from the context (Named a, Read a)

But the following works:

instance Named Wrap where
  name (Wrap named) =
    let a = read undefined
        _ = named a
    in name a

The only difference I see is that a is in covariant position in read, but in contravariant possition in name. Why doesn't the first instance declaration work?


Solution

  • The first instance doesn't work because it doesn't trigger the monomorphism restriction, so a gets a polymorphic type which is instantiated differently in named a and name a. On the other hand, when writing a = read undefined, we find that a has a typeclass-restricted type, so the monomorphism restriction kicks in and we must choose a specific type for a; since named a uniquely identifies this type, that type is chosen and it is not instantiated at a different type in name a.

    You can cause the read version to fail by turning on NoMonomorphismRestriction to verify that this is the correct explanation.

    You can fix the problem by using a lambda instead of a let, as in:

    instance Named Wrap where
        name (Wrap named) = (\a -> (\_ -> name a) (named a)) undefined
    

    (Generally, let x = e in e' is the same as (\x -> e') e, provided x is monomorphic and not recursive, and I have performed this rewrite twice here.)

    However, I would suggest a more serious reworking of your approach to avoid undefined entirely, if possible. The standard trick for this is:

    class Named a where
        name :: proxy a -> String
    
    proxyForFun :: (a -> IO ()) -> Proxy a
    proxyForFun _ = Proxy
    
    instance Named Wrap where
        name (Wrap named) = name (proxyForFun named)
    

    However, the type of name is quite restrictive: it is no longer possible to write instances in which name inspects its argument, so if that was a feature you needed this approach will not work.