Skip to the content.

Implementing an instance of a type class dynamically, part 4

We saw how we can implement an instance of a type class dynamically. Now let’s revisit it with a type class whose method returns an instance head, for example, Monoid.

import Data.Kind (Type)
import Data.Proxy (Proxy (..))
import Data.Reflection (Reifies (..), reify)

type Wrap :: k -> Type -> Type
newtype Wrap s a = Wrap a

type DictMonoid :: Type -> Type
data DictMonoid a = DictMonoid
  { _mempty :: a,
    _mappend :: a -> a -> a
  }

instance (Reifies s (DictMonoid a)) => Semigroup (Wrap s a) where
  (Wrap a1) <> (Wrap a2) = Wrap $ _mappend (reflect (Proxy :: Proxy s)) a1 a2

instance (Reifies s (DictMonoid a)) => Monoid (Wrap s a) where
  mempty = Wrap $ _mempty (reflect (Proxy :: Proxy s))

You can use it like this.

v :: (Reifies s (DictMonoid Int)) => Wrap s Int
v = mempty <> Wrap 10 <> Wrap 20

v1 :: Int
v1 = reify (DictMonoid 0 (+)) $ \(_ :: Proxy s) -> let Wrap n = v @s in n

v2 :: Int
v2 = reify (DictMonoid 1 (*)) $ \(_ :: Proxy s) -> let Wrap n = v @s in n

As you can see, a function passed to reify cannot return Wrap s a because s is an existential type. It means that you always have to unwrap it. Let’s define a new function that unwrap Wrap s a for convenience.

withDictMonoid :: forall a. DictMonoid a -> (forall k (s :: k). (Reifies s (DictMonoid a)) => Wrap s a) -> a
withDictMonoid dictY value = reify dictY $
  \(_ :: Proxy s) -> let Wrap v :: Wrap s a = value in v

withDictMonoid just unwraps a value returned from a function. Callers became a bit cleaner with it.

v3 :: Int
v3 = withDictMonoid (DictMonoid 0 (+)) v

v4 :: Int
v4 = withDictMonoid (DictMonoid 1 (*)) v

Can we generalize withDictMonoid? Yes, we can use a type family to associate a dictionary type and a constraint. Then, we can make withDictMonoid use a proper dictionary type.

import Data.Kind (Constraint, Type)
import Data.Proxy (Proxy (..))
import Data.Reflection (Reifies (..), reify)

type ReifiableConstraint :: (Type -> Constraint) -> Constraint
class ReifiableConstraint c where
  type Dict c a :: Type

type Wrap :: k -> Type -> Type
newtype Wrap s a = Wrap {unwrap :: a}

withDict :: forall c a. Dict c a -> (forall k (s :: k). (Reifies s (Dict c a)) => Wrap s a) -> a
withDict dict value = reify dict $ \(_ :: Proxy s) -> unwrap (value :: Wrap s a)

This requires AllowAmbiguousTypes extension because withDict doesn’t use c itself. It uses c only as Dict c a but the compiler cannot decide what c is from Dict c a because it’s not injective. You need to specify a constraint when you call withDict like withDict @Monoid.

Then, let’s define instances of ReifiableConstriant for Semigroup and Monoid.

type DictMonoid :: Type -> Type
data DictMonoid a = DictMonoid
  { _mempty :: a,
    _mappend :: a -> a -> a
  }

instance ReifiableConstraint Semigroup where
  type Dict Semigroup a = DictMonoid a

instance ReifiableConstraint Monoid where
  type Dict Monoid a = DictMonoid a

instance (Reifies s (DictMonoid a)) => Semigroup (Wrap s a) where
  (Wrap a1) <> (Wrap a2) = Wrap $ _mappend (reflect (Proxy :: Proxy s)) a1 a2

instance (Reifies s (DictMonoid a)) => Monoid (Wrap s a) where
  mempty = Wrap $ _mempty (reflect (Proxy :: Proxy s))

You can now use withDict by specializing it to Monoid constraint.

v :: (Reifies s (DictMonoid Int)) => Wrap s Int
v = mempty <> Wrap 10 <> Wrap 20

v1 :: Int
v1 = withDict @Monoid (DictMonoid 0 (+)) v

v2 :: Int
v2 = withDict @Monoid (DictMonoid 1 (*)) v

We’ve evaluated v with different sets of methods of Semigroup and Monoid.

By the way, you can remove AllowAmbiguousTypes by making Wrap indexed by c as well if you’d like. Then, c will appear in the type signature of withDict as Wrap c s a, and it’s no longer ambiguous.

type ReifiableConstraint :: (Type -> Constraint) -> Constraint
class ReifiableConstraint c where
  type Dict c a :: Type

type Wrap :: (Type -> Constraint) -> k -> Type -> Type
newtype Wrap c s a = Wrap {unwrap :: a}

withDict :: forall c a. Dict c a -> (forall k (s :: k). (Reifies s (Dict c a)) => Wrap c s a) -> a
withDict dict value = reify dict $ \(_ :: Proxy s) -> unwrap (value :: Wrap c s a)

type DictMonoid :: Type -> Type
data DictMonoid a = DictMonoid
  { _mempty :: a,
    _mappend :: a -> a -> a
  }

instance ReifiableConstraint Semigroup where
  type Dict Semigroup a = DictMonoid a

instance ReifiableConstraint Monoid where
  type Dict Monoid a = DictMonoid a

instance (Reifies s (DictMonoid a)) => Semigroup (Wrap Monoid s a) where
  (Wrap a1) <> (Wrap a2) = Wrap $ _mappend (reflect (Proxy :: Proxy s)) a1 a2

instance (Reifies s (DictMonoid a)) => Monoid (Wrap Monoid s a) where
  mempty = Wrap $ _mempty (reflect (Proxy :: Proxy s))

You no longer need to pass @Monoid to withDict with this version because you’ve already specified it to Wrap as Wrap Monoid s Int.

v :: (Reifies s (DictMonoid Int)) => Wrap Monoid s Int
v = mempty <> Wrap 10 <> Wrap 20

v1 :: Int
v1 = withDict (DictMonoid 0 (+)) v

v2 :: Int
v2 = withDict (DictMonoid 1 (*)) v