Skip to the content.

Using GADT as a type function

Imagine that you’d like to write a function that works on different types, such as on Int and Text. You can write this function using a type class.

Then, what can you do when you want to return a different type from this function depending on its argument type? You can write it using a type family.

{-# LANGUAGE FlexibleInstances,
             FunctionalDependencies,
             GADTs,
             MultiParamTypeClasses,
             ScopedTypeVariables,
             TypeApplications,
             TypeFamilies
#-}
{-# OPTIONS -Wall #-}

import Data.Text
import qualified Data.Text as T
import Text.Read (readMaybe)

type family R a where
    R Int = Text
    R Text = Maybe Int

class F a where
    f1 :: a -> R a
instance F Int where
    f1 = T.pack . show
instance F Text where
    f1 = readMaybe . T.unpack

I used a closed type family here to make it consistent with the GADT version, but usually you’ll use an open type family (you’d call it an associated type).

But you can write this function using GADT, too. First, let’s define a GADT that associates an argument type and a return type.

data W a r where
    WInt :: W Int Text
    WText :: W Text (Maybe Int)

Next, let’s write the function. It will take a value of this type in addition to the original argument.

f2 :: W a r -> a -> r
f2 WInt = T.pack . show
f2 WText = readMaybe . T.unpack

When you pass WInt to f2, the compiler unifies a with Int and then r with Text. When you pass WText, a will be unified with Text and r with Maybe Int.

Since WInt and WText is in the value world, you can say that you’re converting a value (WInt) to types (Int and Text).

Instead of passing W a r explicitly, you can let the compiler infer it using a type class.

class WC a r | a -> r where
    w :: W a r
instance WC Int Text where
    w = WInt
instance WC Text (Maybe Int) where
    w = WText

f3 :: forall a r. WC a r => a -> r
f3 x = case w @a of
         WInt -> T.pack $ show x
         WText -> readMaybe $ T.unpack x

When you pass a value of a to f3, the compiler infers W a r using w. Then, it infers r using the functional dependency a -> r.