The reflection
package lets you generate new "local" instances of a typeclass at runtime.
For example, suppose we have the following typeclass of values that can "wrap around":
{-# LANGUAGE Rank2Types, FlexibleContexts, UndecidableInstances #-}
import Data.Reflection
import Data.Proxy
class Wrappy w where
succWrappy :: w -> w
We define this newtype that carries a phantom type parameter:
data WrapInt s = WrapInt { getValue :: Int } deriving Show
An make it an instance of Wrappy
:
instance Reifies s Int => Wrappy (WrapInt s) where
succWrappy w@(WrapInt i) =
let bound = reflect w
in
if i == bound
then WrapInt 0
else WrapInt (succ i)
The interesting part is the Reifies s Int
constraint. It means: "the phantom type s
represents a value of type Int
at the type level". Users never define an instance for Reifies
, this is done by the internal machinery of the reflection
package.
So, Reifies s Int => Wrappy (WrapInt s)
means: "whenever s
represent a value of type Int
, we can make WrapInt s
an instance of Wrappy
".
The reflect
function takes a proxy value that matches the phantom type and brings back an actual Int
value, which is used when implementing the Wrappy
instance.
To actually "assign" a value to the phantom type, we use reify:
-- Auxiliary function to convice the compiler that
-- the phantom type in WrapInt is the same as the one in the proxy
likeProxy :: Proxy s -> WrapInt s -> WrapInt s
likeProxy _ = id
main :: IO ()
main = print $ reify 5 $ \proxy ->
getValue $ succWrappy (likeProxy proxy (WrapInt 5))
Notice that the signature of reify
forbids the phantom type from escaping the callback, that's why we must unwrap the result with getValue
.
See more examples in this answer, on in the reflection GitHub repo.