Why do you need singletons?
Imagine you have a type Value
indexed by ValType
kind, and an existential type SomeValue
that wraps it.
{-# LANGUAGE AllowAmbiguousTypes,
DataKinds,
GADTs,
KindSignatures,
ScopedTypeVariables,
StandaloneDeriving,
TypeApplications
#-}
import Data.Text (Text)
data ValType = Text | Bool
data Value (tag :: ValType) where
T :: Text -> Value 'Text
B :: Bool -> Value 'Bool
deriving instance Show (Value tag)
data SomeValue = forall tag. SomeValue (Value tag)
Now, you want to have a function that unwraps a Value
from a SomeValue
. This can be done by using singletons.
data SValType (tag :: ValType) where
SText :: SValType 'Text
SBool :: SValType 'Bool
class SValTypeI (tag :: ValType) where sing :: SValType tag
instance SValTypeI 'Text where sing = SText
instance SValTypeI 'Bool where sing = SBool
unwrap :: forall tag. SValTypeI tag => SomeValue -> Maybe (Value tag)
unwrap (SomeValue v) =
case sing @tag of
SBool | B _ Just v
SText | T _ Just v
_ -> Nothing
But doesn’t this look redundant? Why can’t you get a ValueType
directly instead of getting SValType
like this?
class IsType (a :: ValType) where typeOf :: ValType
instance IsType 'Text where typeOf = Text
instance IsType 'Bool where typeOf = Bool
unwarp' :: forall tag. IsType tag => SomeValue -> Maybe (Value tag)
unwarp' (SomeValue v) =
case typeOf @tag of
Bool | B _ Just v
Text | T _ Just v
_ -> Nothing
But no, this doesn’t compile. But why?
You can think about this in this way. Passing a typeclass to a function is identical to passing a dictionary of functions to the function. So this unwrap'
is identical to this definition because IsType
only has typeOf
that returns ValType
.
unwarpExplicit' :: ValType -> SomeValue -> Maybe (Value tag)
unwarpExplicit' valType (SomeValue v) =
case valType of
Bool | B _ Just v
Text | T _ Just v
_ -> Nothing
But as you can see, there are no relationships between ValType
and tag
. That’s why this doesn’t compile. valueType
being Bool
doesn’t mean tag
is 'Bool
, and you cannot pattern match v
with B _
.
On the other hand, it’ll become this when you convert the original wrap
to pass a dictionary explicitly.
unwrapExplicit :: SValType tag -> SomeValue -> Maybe (Value tag)
unwrapExplicit sValType (SomeValue v) =
case sValType of
SBool | B _ Just v
SText | T _ Just v
_ -> Nothing
As you can see, SValType tag
has tag
which will be unified with tag
in Maybe (Value tag)
. When you specify a return type of this function, you also fix sValType
. For example, unwrapExplicit SBool (SomeValue (B False)) :: Maybe (Value 'Bool)
compiles, but unwrapExplicit SText (SomeValue (B False)) :: Maybe (Value 'Bool)
doesn’t.
If sValueType
is SBool
, the return type must be Maybe (Value 'Bool)
, and if SText
, it must be Maybe (Value 'Text)
. This makes it possible to pattern match on v
with B _
and T _
.