4. Writing generic functions using variants

In this chapter we show how to write generic functions that can work on different Variant as long as they fulfill some constraints.

4.1. :< and :<< operators

The c :< cs constraint statically ensures that the type c is in the cs type list and that we can set and match it in a variant with type V cs. For example:

newtype Error = Error String

showError :: (Error :< cs) => V cs -> String
showError = \case
   V (Error s) -> "Found error: " ++ s
   _           -> "Not an Error!"

We check that showError works:

e0,e1 :: V '[String,Int,Error]
e0 = V (Error "invalid")
e1 = V @Int 10

> showError e0
"Found error: invalid"

> showError e1
"Not an Error!"

The same generic showError function works with variants of other types as well:

e2 :: V '[Float,String,Maybe Char,Error]
e2 = V (Error "Oups!")

e3 :: V '[Error]
e3 = V (Error "Outch!")

> showError e2
"Found error: Oups!"

> showError e3
"Found error: Outch!"

Note that to shorten a list of constraints such as (A :< xs, B :< xs, C :< xs) you can use the :<< operator: '[A,B,C] :<< xs.

4.2. :<? operator and VMaybe pattern

The c :< cs constraint statically ensures that the type c is in the cs type list. However in some cases we want to write generic functions that work on variants even if they can’t contain the given type.

For instance if we try to apply the showError function of the previous example on a variant that can’t contain a value of type Error, we get the following expected compile-time error:

e4 :: V '[String,Int]
e4 = V "valid"

> showError e4

<interactive>:45:1: error:
    • `Error' is not a member of '[String, Int]

Nevertheless we can write a showErrorMaybe that works on any variant even if it can’t contain an Error value by using the :<? constraint constructor and by matching with VMaybe as follows:

showErrorMaybe :: (Error :<? cs) => V cs -> String
showErrorMaybe = \case
   VMaybe (Error s) -> "Found error: " ++ s
   _                -> "Not an Error!"

> showErrorMaybe e0
"Found error: invalid"

> showErrorMaybe e1
"Not an Error!"

> showErrorMaybe e2
"Found error: Oups!"

> showErrorMaybe e3
"Found error: Outch!"

> showErrorMaybe e4
"Not an Error!"

Obviously this example is a bit contrived because we can easily see that e4 can’t contain an Error. However the same :<? constraint is also used to define some more interesting operations as shown below.

4.3. Shrinking variants: popVariant

A very common use of variants is to pattern match on a specific value type they can contain and to get a new variant containing the left-over value types. This is done with popVariant or popVariantMaybe and the Remove type family. For example:

filterError :: Error :<? cs => V cs -> V (Remove Error cs)
filterError v = case popVariantMaybe v of
   Right (Error s) -> error ("Found error: " ++ s)
   Left  v'        -> v' -- left-over variant!

> filterError e0
*** Exception: Found error: invalid
CallStack (from HasCallStack):
  error, called at Test.hs:61:23 in main:Main

> filterError e1

> :t e1
e1 :: V '[String, Int, Error]

> :t filterError e1
filterError e1 :: V '[String, Int]

> :t e2
e2 :: V '[Float, String, Maybe Char, Error]

> :t filterError e2
filterError e2 :: V '[Float, [Char], Maybe Char]

Notice how an Error value can’t be present anymore in the variant type returned by filterError and how this function is generic as it supports any variant as an input.

Similarly we could have used the Error <: cs constraint and the popVariant function to ensure that only variants that can contain an Error value can be passed to the filterError function.