Skip to the content.

Implementing an instance of a type class dynamically, part 1

We have elem function in base whose type is Eq a => a -> [a] -> Bool if you specialized it to a list (The actual type is (Foldable t, Eq a) => a -> t a -> Bool). Now, is it possible to write a elemBy function whose type is (a -> a -> Bool) -> a -> [a] -> Bool using elem?

elem calls (==) :: a -> a -> Bool in Eq to check if an element is equal to a specified value. elemBy will use the first parameter of type a -> a -> Bool instead of (==). Yes, we want to implement a temporary instance of Eq and let elem use it.

A straightforward way is to declare a type that is a pair of a value and a function, and implement Eq for it.

data WrapEq a = WrapEq a (a -> a -> Bool)

instance Eq (WrapEq a) where
  (WrapEq lhs eq) == (WrapEq rhs _) = eq lhs rhs

elemBy' :: (a -> a -> Bool) -> a -> [a] -> Bool
elemBy' eq e l = elem (WrapEq e eq) $ map (`WrapEq` eq) l

This works, but there is a problem even if we put performance issues aside. As you can see, the implementation of (==) uses a function on the left-hand side, and ignores a function on the right-hand side assuming we’ve put the same function to all the elements. But what happens if we put different functions to each element?

I’m going to look into what we can do about it.

To make it simpler, I’ll define one simple type class named X, and try to implement this type class dynamically instead of Eq.

class X a where
  x :: a -> String

instance X String where
  x = id

instance X Int where
  x = show

Of course, we can invoke x like this.

invoke :: (X a) => a -> String
invoke a = x a

v1 :: String
v1 = invoke (100 :: Int) <> "!!!"

Let’s make it CPS-style.

invokeCPS :: (X a) => a -> (a -> r) -> r
invokeCPS a f = f a

v2 :: String
v2 = invokeCPS (100 :: Int) $ \n -> x n <> "!!!"

Let’s give \n -> x n <> "!!!" a look. A type of this function is (X a) => a -> String. Internally, a type class constraint (X a) => will be converted to an additional parameter of a dictionary of methods in GHC. So its type will be Dict X a -> a -> String for some Dict. In addition to that, Dict X a is a function itself if the type class only has one method. To put them together, the function type becomes identical to (a -> String) -> a -> String.

This means that you can call this function with any function of a -> String if you can cast (X a) => a -> String to (a -> String) -> a -> String. We can do that by wrapping it in a newtype and unsafeCoerce it.

newtype Magic a r = Magic ((X a) => a -> r)

unsafeInvoke :: a -> ((X a) => a -> r) -> r
unsafeInvoke a f = unsafeCoerce (Magic f) (\n -> show (n + 1 :: Int)) a

v3 :: String
v3 = unsafeInvoke (100 :: Int) $ \n -> x n <> "!!!"

v3 will be "101!!!" instead of "100!!!".

You can of course make unsafeInvoke take a function of type a -> String and use it.

unsafeInvoke2 :: (a -> String) -> a -> ((X a) => a -> r) -> r
unsafeInvoke2 x' a f = unsafeCoerce (Magic f) x' a

v4 :: String
v4 = unsafeInvoke2 (\n -> show (n + 2)) (100 :: Int) $ \n -> x n <> "!!!"

Now, once you’ve passed a function \n -> show (n + 2) to unsafeInvoke2, it’ll use this function as an implementation of x of X. v4 will be "102!!!" as you expected.

Another option instead of using unsafe unsafeCoerce is using GHC.Exts.withDict. It does what unsafeCoerce does in a safe manner.

safeInvoke :: forall a r. (a -> String) -> a -> ((X a) => a -> r) -> r
safeInvoke x' a f = withDict @(X a) x' (f a)

v5 :: String
v5 = safeInvoke (\n -> show (n + 3)) (100 :: Int) $ \n -> x n <> "!!!"

They work pretty well, but they only work with a type class with one method. What can we do with a type class with more than one method? We’ll see it in the following posts.