Roll-your-own Scala
I've always really liked this passage from On the Genealogy of Morals:
[T]here is a world of difference between the reason for something coming into existence in the first place and the ultimate use to which it is put, its actual application and integration into a system of goals… anything which exists, once it has come into being, can be reinterpreted in the service of new intentions, repossessed, repeatedly modified to a new use by a power superior to it.
A couple of months ago at LambdaConf I had a few conversations with different people about why we like (or at least put up with) Scala when there are so many better languages out there. Most of the answers were the usual ones: the JVM, the ecosystem, the job market, the fact that you don't have to deal with Cabal, etc.
For me it's a little more complicated than that. I like Scala in part because it's a mess. It's not a "fully" dependently typed language, but you can get pretty close with singleton types and path dependent types. It provides higher-kinded types, but you have to work around lots of bugs and gaps and underspecified behaviors to do anything very interesting with them. And so on—it's a mix of really good ideas and a few really bad ideas and you can put them together in ways that the language designers didn't anticipate and probably don't care about at all.
The rest of this blog post will be a long story about one example of this kind of
thing involving Scalaz's UnapplyProduct
.
Type lambdas🔗
Type lambdas are one example of a Scala "feature" that's both hugely useful (and sometimes necessary) and apparently totally accidental. Suppose we've got a simple functor type class:
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
And we want to create an instance for the List
type constructor. This is easy:
implicit val listFunctor: Functor[List] = new Functor[List] {
def map[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f)
}
Now suppose we want to create an instance for Either[String, ?]
. This is a
little trickier. Scala doesn't provide a direct way to partially apply a
multi-parameter type constructor, but we can define a type alias with the
left-hand type filled in and then use that in our instance definition:
type StringOr[A] = Either[String, A]
implicit val stringOrXFunctor: Functor[StringOr] = new Functor[StringOr] {
def map[A, B](fa: StringOr[A])(f: A => B): StringOr[B] = fa.right.map(f)
}
This trick won't work very well if we want the left-hand type to be generic, though—there's simply no way to fit a type alias between the generic type parameter on our defining method and that method's return type. We could try to put the type alias inside the method and let the return type be inferred:
implicit def eitherFunctor[E] = {
type EOr[A] = Either[E, A]
new Functor[EOr] {
def map[A, B](fa: EOr[A])(f: A => B): EOr[B] = fa.right.map(f)
}
}
But that won't actually work:
scala> type StringOr[A] = Either[String, A]
defined type alias StringOr
scala> implicitly[Functor[StringOr]]
<console>:14: error: could not find implicit value for parameter e: Functor[StringOr]
implicitly[Functor[StringOr]]
^
Things are looking pretty bleak, but there is a way we can define this instance. It's just not the kind of thing you're likely to guess would work if you hadn't seen it before.
implicit def eitherFunctor[E]: Functor[({ type L[x] = Either[E, x] })#L] =
new Functor[({ type L[x] = Either[E, x] })#L] {
def map[A, B](fa: Either[E, A])(f: A => B): Either[E, B] = fa.right.map(f)
}
This horrible syntax is an abuse of two Scala language features that at first seem completely unrelated to the task at hand: type refinements and type projections. These language features seem completely unrelated to this problem at least in part because they were designed for completely unrelated purposes—see for example this comment by Paul Phillips on a Scala issue I opened in 2012:
Type lambdas are cool and all, but not a single line of the compiler was ever written with them in mind. They're just not going to work right: the relevant code is not robust.
This is one of the nice things about creating cool stuff by accident—you don't have to support it (even when it becomes an idiomatic part of the language—type lambdas for example have had special support in IntelliJ for years, and kind-projector, a compiler plugin that provides syntactic sugar for type lambdas, is becoming increasingly widely used).
SI-2712🔗
Now that we've got a functor instance for any Either[E, ?]
we probably want to
be able to use it in methods like the following:
def incrementInside[F[_]: Functor](fa: F[Int]): F[Int] =
implicitly[Functor[F]].map(fa)(_ + 1)
Which works like this on lists:
scala> val myList = List(1, 2, 3)
myList: List[Int] = List(1, 2, 3)
scala> val myIncrementedList = incrementInside(myList)
myIncrementedList: List[Int] = List(2, 3, 4)
And like this on Either
:
val myEither: Either[String, Int] = Right(1)
val myIncrementedEither: Either[String, Int] = incrementInside(myEither)
Just kidding, that doesn't actually compile. Scala isn't capable of figuring out
that Either[String, Int]
can be seen as a F[Int]
where F
is
Either[String, ?]
(which of course we have a functor instance for).
This limitation is tracked in SI-2712, an issue that will celebrate its sixth birthday this November (it's also one of the five or six Scala issues that I can recognize by number).
Fortunately there's (another) workaround, due originally to Miles Sabin and now available in both Scalaz and cats:
trait Unapply[TC[_[_]], MA] {
type M[_]
type A
def instance: TC[M]
def apply(ma: MA): M[A]
}
object Unapply {
implicit def unapplyMA[TC[_[_]], M0[_], A0](implicit
instance0: TC[M0]
): Unapply[TC, M0[A0]] {
type M[X] = M0[X]
type A = A0
} = new Unapply[TC, M0[A0]] {
type M[X] = M0[X]
type A = A0
def instance = instance0
def apply(ma: M0[A0]): M[A] = ma
}
implicit def unapplyMAB[TC[_[_]], M0[_, _], A0, B0](implicit
instance0: TC[({ type L[x] = M0[A0, x] })#L]
): Unapply[TC, M0[A0, B0]] {
type M[x] = M0[A0, x]
type A = B0
} = new Unapply[TC, M0[A0, B0]] {
type M[x] = M0[A0, x]
type A = B0
def instance = instance0
def apply(ma: M0[A0, B0]): M[A] = ma
}
}
Now we can rewrite our incrementInside
method like this:
def incrementInside[FA](fa: FA)(implicit
U: Unapply[Functor, FA] { type A = Int }
): U.M[Int] = U.instance.map(U(fa))(_ + 1)
And then:
scala> incrementInside(myList)
res4: List[Int] = List(2, 3, 4)
scala> incrementInside(myEither)
res5: scala.util.Either[String,Int] = Right(2)
Fortunately in practice we usually only need to define methods that take an
Unapply
instance for a small handful of generic operations (a common
convention is to add a U
to the end of the method name to distinguish these
versions—so for example in Scalaz the syntax for Traverse
provides both
traverse
and traverseU
methods).
So now we've used implicit resolution to work around a limitation involving higher-order unification for a type class instance that we defined using type refinements and type projections to work around a limitation involving partial application of type parameters. Next we'll add even more dead body parts to the monster.
Multiple implicit parameter sections🔗
Sometimes we need more complex Unapply
machinery than the relatively simple
version above. One example is syntax for things that are bitraversable. For
example, we might want to take a tuple (or Either
) and do some kind of logging
in a Writer
monad for both sides (or either):
import scalaz._, Scalaz._
(1, 'a).bitraverseU(_.set("saw int" :: Nil), _.set("saw sym" :: Nil))
This in effect lets us map two functions returning writers over the two sides of
our tuple and then turn the whole thing inside out, giving us a
writer that logs to a list of strings and returns a (Int, Symbol)
.
We need the bitraverseU
version again
because of SI-2712—the Scala compiler isn't able to recognize the pieces it
needs in the return types of the functions we're mapping over the two sides.
Unfortunately this doesn't actually work in Scalaz at the moment. It relies on an
Unapply
variant called UnapplyProduct
that was added to Scalaz in 2011 by
Jason Zaugg, but it's always been necessary to construct these
instances by hand (which is extremely inconvenient and boilerplate-y). From
Jason's original comments, which are still in the source today:
// This seems to motivate multiple implicit parameter sections. Is there another way?
And:
// Would be nice, but I'm not sure we can conjure UnapplyProduct implicitly, at least without multiple implicit
// parameter lists.
The issue is that the signature for the implicit method that generates
UnapplyProduct
instances seems to need to look like the following, since we
have to confirm that the M
types in our Unapply
instances for the left and
right sides are the same:
implicit def unapply[TC[_[_]], MA0, MB0](implicit
U1: Unapply[TC, MA0],
U2: Unapply[TC, MB0]
)(implicit iso: U1.M <~> U2.M): UnapplyProduct[TC, MA0, MB0] { // ...
Scala doesn't allow types to be referred to by other types in the same parameter
list, so we can't just stick the <~>
in with the other instances. The problem
is that Scala also doesn't support multiple implicit parameter lists. This is
a more arbitrary limitation, but I've never seen any serious discussion of
changing it.
So we're out of luck. I should know by now that if Jason Zaugg is asking for
help on a problem, I should stay away from it, but over the past three or four
years I've burned at least two or three Saturdays trying to find a way to
implement this unapply
method.
More workarounds!🔗
In February Miles Sabin posted a gist
demonstrating what he called "a new approach to encoding dependently-typed
chained implicits, using singleton types". It's a pretty clever trick, and my
first thought when I saw it was that we could use it to fix Scalaz's UnapplyProduct
. And it
worked, although I
didn't get around to submitting a pull request until this week.
The implementation looks like this:
case class SingletonOf[T, U <: { type A; type M[_] }](
widen: T { type A = U#A; type M[x] = U#M[x] }
)
object SingletonOf {
implicit def mkSingletonOf[T <: { type A; type M[_] }](implicit
t: T
): SingletonOf[T, t.type] = SingletonOf(t)
}
implicit def unapply[
TC[_[_]],
MA0,
MB0,
U1 <: { type A; type M[_] },
U2 <: { type A; type M[_] }
](implicit
sU1: SingletonOf[Unapply[TC, MA0], U1],
sU2: SingletonOf[Unapply[TC, MB0], U2],
iso: U1#M <~> U2#M
): UnapplyProduct[TC, MA0, MB0] {
type M[x] = U1#M[x]
type A = U1#A
type B = U2#A
} = new UnapplyProduct[TC, MA0, MB0] {
type M[x] = U1#M[x]
type A = U1#A
type B = U2#A
def TC = sU1.widen.TC
def _1(ma: MA0) = sU1.widen(ma)
def _2(mb: MB0) = iso.from(sU2.widen(mb))
}
The key idea is that the U1
and U2
type parameters will be inferred to be
the singleton types for our two Unapply
instances, and our SingletonOf
type
class captures the way that these types line up with our TC
, MA0
, and MB0
.
This allows us to refer to the M
type parameters for the Unapply
instances in
the same implicit parameter list that gathers the Unapply
instances themselves
(or rather their SingletonOf
wrappers).
Note that SingletonOf
here is less generic than Miles's original formulation,
which would also work if we were willing to do some (safe) casting. Since this
is the only interesting application that I can think of for this trick, I'm not worried about
using a less generic formulation.
To demonstrate that it works, you can check out my pull request and run something like the following:
import scalaz._, Scalaz._
val w = (1, 'a).bitraverseU(_.set("saw int" :: Nil), _.set("saw sym" :: Nil))
And then:
scala> w.run
res0: (List[String], (Int, Symbol)) = (List(saw int, saw sym),(1,'a))
To recap: we've taken a few basic (but still pretty broken) Scala language features—singleton types, refinement types, type projections, and the implicit resolution system—and we've very painfully built a language for ourselves that at least kind of looks like it supports partial type parameter application, higher-order unification, and multiple implicit parameter sections.
I think that's pretty neat, but I can also understand why almost everyone else would find it horrifying.