Higher Leibniz

Strictly necessarily strict

The word “witness” implies that Leibniz is a passive bystander in
your function; sitting back and telling you that some type is equal to
another type, otherwise content to let the real code do the real
work. The fact that Leibniz lifts into functions (which are a
member of the everything set, you’ll agree) might reinforce the
notion that Leibniz is spooky action at a distance.

But one of the nice things about Leibniz is that there’s really no
cheating: the value with its shiny new type is dependent on the
Leibniz actually existing, and its subst, however much a glorified
identity function it might be, completing successfully.

To see this in action, let’s check in with the bastion of not
evaluating stuff, Haskell.

The id from refl? The type-substituted data actually goes
through that function. The same goes for the subst method in
Scala.

When using Leibniz combinators, the strictness forms a chain to
all underlying Leibniz evidence. If there are any missing
values, the transform will also fail.

Higher kinded Leibniz

Let’s try a variant on Leib.

sealedabstractclassLeibF[G[_], H[_]]{defsubst[F[_[_]]](fa:F[G]):F[H]}

This reads “LeibF[G, H] can replace G with H in any type
function”. But, whereas the
kind
of the types that Leib discusses is *, for LeibF it’s *->*. So,
LeibF[List, List] exhibits that the type constructorsList and
List are equal.

In Haskell, we can take advantage of the fact that the actual
implementations are kind-agnostic, by having those definitions be
applicable to all kinds via
the PolyKinds language extension,
mentioned at the top of the Haskell code above. No such luck in
Scala.

Better GADTs

In a post from a couple months ago,
Kenji Yoshida outlines an interesting way to simulate the missing
type-evidence features of Scala’s GADT support with Leibniz. This
works in Haskell, too, in case you are comfortable with turning on
RankNTypes
but not
GADTs
somehow.

Note that the Haskell type system understands that when hoge’s first
argument’s data constructor is X, the type variables a and b
must be the same type, and therefore by implication the argument of
type f a c must also be of type f b c. This is what we’re trying
to get Scala to understand.

The overridden cata method

Kenji introduces a cata method on Foo to constrain use of the
Leibniz.force hack, while still providing external code with usable
Leibniz evidence that can be lifted to implement hoge. However,
by implementing the method in a slightly different way, we can use
refl instead.

We supplied A for both the A and B type parameters in our
extends clause, so that substitution also applies in all methods
from Foo that we’re implementing, including cata. At that point
it’s obvious to the compiler that refl implements the requested
Leib.

Incidentally, a similar style of substitution underlies the definition
of refl.

The Leib member

What if we don’t want to write or maintain an overriding-style cata?
After all, that’s an n² commitment. Instead, we can incorporate a
Leib value in the GADT. First, let’s see what the equivalent
Haskell is, without the GADTs extension:

It feels a little weird that X now must retain Foo’s
type-system-level separation of the two type parameters. But this
style may more naturally integrate in your ADTs, and it is much closer
to the original non-working hoge1 implementation.

It also feels a little weird that you have to waste a slot carting
around this evidence of type equality. As demonstrated in section
“It’s really there” above, though, it matters that the instance
exists.

You can play games with this definition to make it easier to supply
the wholly mechanical leib argument to X, e.g. adding it as an
implicit val in the second parameter list so it can be imported and
implicitly supplied on X construction. The basic technique is
exactly the same as above, though.