Polymorphic variants for PureScript.
bower install purescript-variant
- Module documentation is published on Pursuit.
Data.Variant
is an implementation of polymorphic variants in PureScript. What
are polymorphic variants? Before we get to that, lets look at the dual, which
you are likely familiar with if you've been using PureScript: records.
Another name for records might be polymorphic products. A product is simply
data that holds inhabitants for more than one type at a time, Tuple a b
being
the canonical product.
data Tuple a b = Tuple a b
If I have a Tuple Int String
, then I have available some Int
value paired
with a String
value (or Int * String
, thus a product). For convenience,
we often like to use records, especially for models in our shiny web apps.
type User =
{ name :: String
, age :: Int
, email :: Email
}
And maybe use it like so:
addJrSuffix :: User -> User
addJrSuffix user = user { name = user.name <> ", Jr." }
However this type signature is needlessly specific. In fact, all it cares about is
the name
field. We can express this sort of structural typing in PureScript
via row types:
addJrSuffix :: forall r. { name :: String | r } -> { name :: String | r }
addJrSuffix hasName = hasName { name = hasName.name <> ", Jr." }
Now I can pass in anything that merely has a name :: String
.
addJrSuffix { name: "Bob" }
addJrSuffix { age: 42, name: "Gerald" }
So records, or polymorphic products, let us pass in anything as long as it has the structure we specify, and even get the same structure back after we are done with it.
Let's flip back around to sum types (or variants), Either a b
being the
canonical dual of Tuple a b
.
data Either a b = Left a | Right b
Where Tuple a b
says we have an a
paired with a b
, Either a b
says we
have either an a
or a b
via the Left
and Right
constructors. We'd
handle the possibilities by pattern matching on it with case
.
This library just uses the same structural row system that we use with records (products) and applies them to variants (sums). Voila!
We lift values into Variant
with inj
by specifying a tag.
someFoo :: forall v. Variant (foo :: Int | v)
someFoo = inj (SProxy :: SProxy "foo") 42
SProxy
is just a way to tell the compiler what our tag is at the type level.
I can stamp out a bunch of these with different labels:
someFoo :: forall v. Variant (foo :: Int | v)
someFoo = inj (SProxy :: SProxy "foo") 42
someBar :: forall v. Variant (bar :: Boolean | v)
someBar = inj (SProxy :: SProxy "bar") true
someBaz :: forall v. Variant (baz :: String | v)
someBaz = inj (SProxy :: SProxy "baz") "Baz"
We can try to extract a value from this via on
, which takes a function to
handle the inner value in case of success, and a function to handle the rest in
case of failure.
fooToString :: forall v. Variant (foo :: Int | v) -> String
fooToString = on (SProxy :: SProxy "foo") show (\_ -> "not foo")
fooToString someFoo == "42"
fooToString someBar == "not foo"
We can chain usages of on
and terminate it with case_
(for compiler-checked
exhaustivity) or default
(to provide a default value in case of failure).
_foo = SProxy :: SProxy "foo"
_bar = SProxy :: SProxy "bar"
_baz = SProxy :: SProxy "baz"
allToString :: Variant (foo :: Int, bar :: Boolean, baz :: String) -> String
allToString =
case_
# on _foo show
# on _bar (if _ then "true" else "false")
# on _baz (\str -> str)
someToString :: forall v. Variant (foo :: Int, bar :: Boolean | v) -> String
someToString =
default "unknown"
# on _foo show
# on _bar (if _ then "true" else "false")
allToString someBaz == "Baz"
someToString someBaz == "unknown"
Handlers with on
are also compositional! We can compose them together with
function composition and reuse them in different contexts.
onFooOrBar :: forall v. (Variant v -> String) -> Variant (foo :: Int, bar :: Boolean | v) -> String
onFooOrBar = on _foo show >>> on _bar (if _ then "true" else "false")
allToString :: Variant (foo :: Int, bar :: Boolean, baz :: String) -> String
allToString =
case_
# onFooOrBar
# on _baz (\str -> str)
Instead of chaining with just on
, there is onMatch
which adds record sugar.
onFooOrBar :: forall v. (Variant v -> String) -> Variant (foo :: Int, bar :: Boolean | v) -> String
onFooOrBar = onMatch
{ foo: show :: Int -> String
, bar: if _ then "true" else "false"
}
But note that polymorphic functions like show
or id
need to be either
annotated or eta expanded due to record impredicativity.
onMatch
can be used with case_
and default
just like on
, but there is
also match
for the common case of total matching.
allToString :: Variant (foo :: Int, bar :: Boolean, baz :: String) -> String
allToString = match
{ foo: \a -> show a
, bar: \a -> if a then "true" else "false"
, baz: \a -> a
}
We can combine polymorphic variants with Functor
s as well using VariantF
,
which lives in Data.Functor.Variant
. VariantF
is just like Variant
,
except it's indexed by things of kind Type -> Type
.
someFoo :: forall v. VariantF (foo :: FProxy Maybe | v) Int
someFoo = inj (SProxy :: SProxy "foo") (Just 42)
someBar :: forall v. VariantF (bar :: FProxy (Tuple String) | v) Int
someBar = inj (SProxy :: SProxy "bar") (Tuple "bar" 42)
someBaz :: forall v a. VariantF (baz :: FProxy (Either String) | v) a
someBaz = inj (SProxy :: SProxy "baz") (Left "Baz")
VariantF
supports all the same combinators as Variant
. We need to use
FProxy
in the types, however, because the row machinery in PuresScript
is not poly-kinded. FProxy
lets us talk about functors, but trick the
type system into thinking they are the expected kind.