Skip to the content.

Functions taking some types, part 1

Imagine you have type X indexed by a phantom type S.

{-# LANGUAGE DataKinds,
             EmptyCase,
             FlexibleContexts,
             GADTs,
             InstanceSigs,
             MultiParamTypeClasses,
             PolyKinds,
             ScopedTypeVariables,
             StandaloneDeriving,
             TemplateHaskell,
             TypeApplications,
             TypeFamilies,
             UndecidableInstances
#-}
{-# OPTIONS_GHC -Wincomplete-patterns #-}

import Data.Kind
    ( Constraint
    , Type
    )
import Data.Singletons.Prelude
    ( Elem
    , If
    )
import Data.Singletons.TH
import Data.Text (Text)
import qualified Data.Text as T

singletons [d|
    data S = S1 | S2 | S3 | S4 deriving (Show, Eq)
  |]

data X :: S -> Type where
    X1 :: X 'S1
    X2 :: Int -> X 'S2
    X3 :: Text -> X 'S3
    X4 :: Float -> X 'S4

deriving instance Show (X s)

You want to define a function taking X with some of S. For example, that function takes X 'S2 and X 'S3, but doesn’t take X 'S1 nor X 'S4. How do you write this function?

The simplest approach would be to use Either. Your function will take Either (X 'S2) (X 'S3).

f1 :: Either (X 'S2) (X 'S3) -> Text
f1 (Left (X2 n)) = T.pack $ show n
f1 (Right (X3 t)) = t

You can call it by wrapping your X by Left or Right.

c1 :: Text
c1 = f1 $ Left $ X2 2

This is pretty straightforward, but you need to nest Either when you make this function take X 'S4 too, which is a bit annoying.

How about using a constraint? You can define a type function (type family) of kind S -> Constraint and use it as a constraint of the function.

type family F2 s :: Constraint where
    F2 'S2 = ()
    F2 'S3 = ()
    F2 _ = ('True ~ 'False)

f2 :: F2 s => X s -> Text
f2 (X2 n) = T.pack $ show n
f2 (X3 t) = t

c2 :: Text
c2 = f2 $ X2 2

As you know, applying a type constraint to a function is the same thing as passing a table explicitly. You’ll define a GADT of kind S -> Type (instead of defining a type constraint of kind S -> Constraint) to do this.

data F3 :: S -> Type where
    F32 :: F3 'S2
    F33 :: F3 'S3

f3 :: F3 s -> X s -> Text
f3 F32 (X2 n) = T.pack $ show n
f3 F33 (X3 t) = t

c3 :: Text
c3 = f3 F32 $ X2 2

In this approach, you need to pass F3 explicitly to f3 whereas it was implicitly passed to f2. Can we make it implicit? Yes, first, let’s define Proved type class.

class Proved p a where
    auto :: p a

Then, make F3 instances of this class.

instance Proved F3 'S2 where
    auto = F32

instance Proved F3 'S3 where
    auto = F33

Then, you can always pass auto as its first parameter and the complier will find a proper instance.

f3' :: Proved F3 s => X s -> Text
f3' = f3 auto

c3' :: Text
c3' = f3' $ X2 2

f3' is a bit generic version of f2 but they do the same thing.

If you think it’s cumbersome to write F2 or F3, you could make a type of the function take a list of types directly by writing a helper constraint OneOf.

type family OneOf t l :: Constraint where
    OneOf t l = If (Elem t l) (() :: Constraint) ('True ~ 'False)

f4 :: OneOf s '[ 'S2, 'S3 ] => X s -> Text
f4 (X2 n) = T.pack $ show n
f4 (X3 t) = t

c4 :: Text
c4 = f4 $ X2 2