Union Type in Idris (Part 3)

Posted on July 28, 2016 by Nicolas Biri

Go back to the first part.

Some are better than others, Union is better than sum

At this point, we have a slightly less complex syntax than sum types (with some drawbacks, but I will detail them in another part), but did we have other benefits?

Well, yes.

Today, we are going to inverstigate how flexible union types are. More precisely, I will detail union type restrictions and generalisation.

Shrink my union

Let suppose that we have a variable x of type Union [Nat, String, List String] and the following repl session:

> the (Maybe Nat) x
Nothing : Maybe Nat

We know that x does not contain a Nat. Thus, x contains either a String or a List String. Well, we can explicitly express it with union types.

Let’s compute the type of this reasoning. Given an union instance, we can either get a value of a type in this uion or we can restrict the union. Quite clear and easy to write:

retract : Union xs -> {auto p: Elem ty xs}
                   -> Either (Union (dropElem xs p)) ty

The result type may require some explanations. The presence of Either and the right case (ty) is clear. The left case (dropElem xs p) is straightforward if we look at the dropElem documentation: it removes the element pointed by p from the list.

The implementation is almost as easy to write as the type:

retract : Union xs -> {auto p: Elem ty xs}
                  -> Either (Union (dropElem xs p)) ty
retract (MemberHere x) {p = Here} = Right x
retract (MemberHere x) {p = (There _)} =
  Left (MemberHere x)
retract (MemberThere x) {p = Here} =
  Left x
retract (MemberThere x) {p = (There later)} =
  either (Left . MemberThere)
         Right
         $ retract x {p = later}

The 3 first cases are trivial, they can almost be completed automatically by Idris (I should talk about this really cool feature in a next post). The last case is a bit more complex. The idea is to lift the result of retract to the next step: on a right, propagate the found value, on a left, just buried it one step further.

It’s time to see retract in action:

> :let x = the (Union [Nat, String, List String]) $ member "foo"
> the (Either _ Nat) $ retract x
Left (MemberHere "foo") : Either (Union [String, List String]) Nat
> the (Either _ String) $ retract x
Right "foo" : Either (Union [Nat, List String]) String

Enlarge my union!

Can we do the opposite of retract? If I have a member of Union [Nat, List String], can we claim that we have a member of a broader union?

Yes, we can but it is a bit more complex than the retract case. The main reason is that we have an infinity of target types for the result union.

Identifying the broader unions

The objective is to define a data type that contain a proof that an union is broader than another one. Or, by transitivity, that each element of a list of types is contained in another list. This is as simple as it sounds. I mean, really, if its sounds complicated to you, it will be complicated to read, if it sounds simple, it will be simple to read:

data Sub : List a -> List a -> Type where
  SubZ : Sub [] ys
  SubK : Sub xs ys ->  Elem ty ys -> Sub (ty::xs) ys

To build a Sub type, we must be able proof that each element of the first list are in the second one.

Trying to generalize union

Now that we have a definition of Sub, we can type our generalize function more easily:

generalize : (u: Union xs) -> {auto s: Sub xs ys}
                           -> Union ys

Given an union, if we have a proof that the list of type in the union is a subset of the list of type in the resulting union, we can generalize the union. Yes, it’s that easy.

Things are getting more complex when we want to implement generalize. Let’s start with a naive implementation and see how it goes:

generalize : (u: Union xs) -> {auto s: Sub xs ys}
                           -> Union ys
generalize (MemberHere x) = member x
generalize (MemberThere x) {s = (SubK y z)} =
  generalize x {s=y}

If we have at MemberHere, x is the value of the union, and thus, must be put in the result union, and we use previously defined member to do so. If we haven’t find the value yet, we just restart one step further.

This version reallly seemed ok to me. Unfortunately, it didn’t match the compiler expactations:

When checking right hand side of generalize with expected type
        Union ys

When checking argument p to function Data.UnionType.member:
        Can't find a value of type
                Elem ty ys

Almost there, the sole issue is that Idris wasn’t able to compute the new location of the value. Fortunately, this location is carried along by Sub and can be easily added:

generalize : (u: Union xs) -> {auto s: Sub xs ys}
                           -> Union ys
generalize (MemberHere x) {s = (SubK _ z)} =
  member x {p = z}
generalize (MemberThere x) {s = (SubK y _)} =
  generalize x {s=y}

And here we go:

> :let x = the (Union [Nat, String]) $ member 2
> the (Union [String, Nat, List String]) $ generalize x
MemberThere (MemberHere 2) : Union [String, Nat, List String]

Part 3 is over

That’s all for today.