5. Converting variants

5.1. Converting from/to a value

We can easily convert between a variant with a single value type and this value type with variantToValue and variantFromValue:

intV :: V '[Int]
intV = V @Int 10

> variantToValue intV
10

> :t variantToValue intV
variantToValue intV :: Int

> :t variantFromValue "Test"
variantFromValue "Test" :: V '[String]

variantFromValue is especially useful to avoid having to define the value types of the variant explicitly.

5.2. Converting from/to Either

variantFromEither and variantToEither can be used to convert between a variant of arity 2 and the Either data type:

eith :: Either Int String
eith = Left 10

> :t variantFromEither eith
variantFromEither eith :: V '[String, Int]

x,y :: V '[String,Int]
x = V "test"
y = V @Int 10

> variantToEither x
Right "test"

> variantToEither y
Left 10

5.3. Extending

We can extend the value types of a variant by appending or prepending a list of types with appendVariant and prependVariant:

x :: V '[String,Int]
x = V "test"

data A = A
data B = B

px = prependVariant @'[A,B] x
ax = appendVariant @'[A,B] x

> :t ax
ax :: V '[String, Int, A, B]

> :t px
px :: V '[A, B, String, Int]

You can use the Concat type family to specify the type of a concatened variant:

data Error0 = Error0 deriving Show
data Error1 = Error1 deriving Show

checkErr ::
   ( Int :< is
   , os ~ Concat is '[Error0,Error1]
   , Error0 :< os
   , Error1 :< os
   ) => V is -> V os
checkErr = \case
   V (0 :: Int) -> V Error0
   V (1 :: Int) -> V Error1
   v            -> appendVariant @'[Error0,Error1] v

> checkErr (V @Int 0 :: V '[Float,Int])
V @Error0 Error0

> checkErr (V @Int 1 :: V '[Float,Int])
V @Error1 Error1

> checkErr (V @Int 2 :: V '[Float,Int])
V @Int 2

> checkErr (V @Float 5.0 :: V '[Float,Int])
V @Float 5.0

> z = checkErr (V @Float 5.0 :: V '[Float,Int,String,Double])
> :t z
z :: V '[Float, Int, [Char], Double, Error0, Error1]

Appending and prepending are very cheap operations: appending just messes with types and performs nothing at runtime; prepending only increases the tag value at runtime by a constant number.

5.4. Extending and reordering: lifting

We can extend and reorder the value types of a variant with the liftVariant function:

x :: V '[String,Int]
x = V "test"

-- adding Double and Float, and reordering
y :: V '[Double,Int,Float,String]
y = liftVariant x

You can use the LiftVariant is os constraint to write generic code and to ensure that the type list is is a subset of os:

liftX :: (LiftVariant is (Double : Float : is))
      => V is -> V (Double : Float : is)
liftX = liftVariant

> :t liftX x
liftX x :: V '[Double, Float, String, Int]

> :t liftX (V "test" :: V [String])
liftX (V "test" :: V [String]) :: V [Double, Float, String]

5.5. Nubing

If the list of value types of a variant contains the same type more than once, we can decide to only keep one of them with nubVariant:

> z = nubVariant (V "test" :: V '[String,Int,Double,Float,Double,String])
> :t z
z :: V '[String, Int, Double, Float]

You can use the Nub type family to write generic code.

5.6. Flattening

If the value types of a variant are themselves variants, you can flatten them with flattenVariant:

x :: V '[String,Int]
x = V "test"

nest :: V '[ V '[String,Int], V '[Float,Double]]
nest = V x

> :t flattenVariant nest
flattenVariant nest :: V '[String, Int, Float, Double]

You can use the Flattenable type-class and the FlattenVariant type family to write generic code.

5.7. Joining

We can transform a variant of functor values (e.g., V '[m a, m b, m c]) into a single functor value (e.g., m (V '[a,b,c])) with joinVariant:

fs0,fs1,fs2 :: V '[ Maybe Int, Maybe String, Maybe Double]
fs0 = V @(Maybe Int) (Just 10)
fs1 = V (Just "Test")
fs2 = V @(Maybe Double) Nothing

> joinVariant @Maybe fs0
Just (V @Int 10)

> joinVariant @Maybe fs1
Just (V @[Char] "Test")

> joinVariant @Maybe fs2
Nothing

It also works with IO for example:

printRet :: Show a => a -> IO a
printRet a = do
   print a
   return a

ms0,ms1 :: V '[ IO Int, IO String, IO Double]
ms0 = V @(IO Int) (printRet 10)
ms1 = V (printRet "Test")

> joinVariant @IO ms0
10
V @Int 10

> joinVariant @IO ms1
"Test"
V @[Char] "Test"

> :t joinVariant @IO ms0
joinVariant @IO ms0 :: IO (V '[Int, String, Double])

Writing generic code requires the use of the JoinVariant m xs constraint and the resulting list of value types can be obtained with the ExtractM m xs type family.

> :t joinVariant
joinVariant :: JoinVariant m xs => V xs -> m (V (ExtractM m xs))

Note

With IO it is possible to use the joinVariantUnsafe function which doesn’t require the type application and doesn’t use the JoinVariant type-class. However some other functor types aren’t supported (e.g., Maybe) and using joinVariantUnsafe with them makes the program crash at runtime.

5.8. Combining two variants (product)

We can combine two variants into a single variant containing a tuple with productVariant:

fl :: V '[Float,Double]
fl = V @Float 5.0

d :: V '[Int,Word]
d = V @Word 10

dfl = productVariant d fl

> dfl
V @(Word,Float) (10,5.0)

> :t dfl
dfl :: V '[(Int, Float), (Int, Double), (Word, Float), (Word, Double)]

5.9. Converting to tuple/HList

We can convert a Variant into a tuple of Maybes with variantToTuple:

w,k,u :: V '[String,Int,Double,Maybe Int]
w = V @Double 1.0
k = V (Just @Int 10)
u = V @Int 17

> :t variantToTuple w
variantToTuple w :: (Maybe String, Maybe Int, Maybe Double, Maybe (Maybe Int))

> variantToTuple w
(Nothing,Nothing,Just 1.0,Nothing)

> variantToTuple k
(Nothing,Nothing,Nothing,Just (Just 10))

> variantToTuple u
(Nothing,Just 17,Nothing,Nothing)

And similarly into an HList (heterogeneous list) with variantToHList:

> variantToHList w
H[Nothing,Nothing,Just 1.0,Nothing]

> variantToHList k
H[Nothing,Nothing,Nothing,Just (Just 10)]

> variantToHList u
H[Nothing,Just 17,Nothing,Nothing]