Skip to the content.

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 _.