Skip to the content.

Combining two functions in a type-safe way

Imagine that you have types A and B, a function returns A or B depending on a Bool value, and a function returning a function from A to String or B to String.

data A = A
data B = B

value :: Bool -> Either A B
value True = Left A
value False = Right B

func :: Bool -> Either (A -> String) (B -> String)
func True = Left $ \A -> "A"
func False = Right $ \B -> "B"

Now you have a function that takes a Bool and returns a String. This function calls value and pass its return value to func.

test :: Bool -> String
test useA = case (value useA, func useA) of
  (Left a, Left fa) -> fa a
  (Right b, Right fb) -> fb b
  _ -> error "Never happens"

The case isn’t exhaustive even though the error case won’t happen.

Can you make this type-safe?

The first thing you need to do is making value and func not take a Bool value, but take one of two types so that it can return a value of a different type based on it. Since the return type cannot depend on a value (True or False), you need to pass different types.

You can use lifted types 'True and 'False for it, but you cannot use them directly because their kind is Bool, not Type.

So now, we’ll create a type named SBool that takes 'True or 'False. Since its kind is Type (when you pass Bool), you can use it as a type.

{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}

import Data.Kind (Type)

data A = A
data B = B

type SBool :: Bool -> Type
data SBool b where
  STrue :: SBool 'True
  SFalse :: SBool 'False

As you can see, passing STrue means b is 'True, and passing SFalse means b is 'False. So we can now pass either of 'True or 'False wrapped in SBool.

Next, we need a type function that returns A if its parameter is 'True, and B if 'False.

type R :: Bool -> Type
type family R b where
  R 'True = A
  R 'False = B

With them, you can now write value and func. You’ll pass b in SBool to R to get a desired type. For example, when you pass STrue whose type is SBool 'True, a return type of value becomes R 'True which is A.

value :: SBool b -> R b
value STrue = A
value SFalse = B

func :: SBool b -> R b -> String
func STrue A = "A"
func SFalse B = "B"

Before implementing test, we need one more type; an existential type to wrap SBool. Since SBool 'True and SBool 'False are different types, you cannot mix them without it.

data SomeBool where
  SomeBool :: SBool b -> SomeBool

In test, we’ll convert Bool to SBool, then pass it to value and func. The compiler now knows value returns A and func returns A -> String, or B and B -> String, you can pass a value returned from value to func in a type-safe manner.

test :: Bool -> String
test useA =
  let sc = if useA then SomeBool STrue else SomeBool SFalse
   in case sc of
        SomeBool sb -> func sb (value sb)