The Limitations of Type Classes as Subtyped Implicits

The Limitations of Type Classes as Subtyped Implicits

Paper: https://adelbertc.github.io/publications/typeclasses-scala17.pdf

Type classes enable a powerful form of ad-hoc polymorphism which provide solutions to many programming design problems. Inspired by this, Scala programmers have striven to emulate them in the design of libraries like Scalaz and Cats.

The natural encoding of type classes combines subtyping and implicits, both central features of Scala. However, this encoding has limitations. If the type class hierarchy branches, seemingly valid programs can hit implicit resolution failures. These failures must then be solved by explicitly passing the implicit arguments which is cumbersome and negates the advantages of type classes.

In this paper we describe instances of this problem and show that they are not merely theoretical but often arise in practice. We also discuss and compare the space of solutions to this problem in Scala today and in the future.

Fb8e986500c5059b2a6c0b2184bb0faf?s=128

Adelbert Chang

October 22, 2017
Tweet

Transcript

  1. 2.

    Type classes • Ad-hoc polymorphism in Haskell (Wadler and Blott

    1989) • classes and instances vs. function overloading • Automatic dictionary/instance look-up (similar to implicits) • Natural tool for building up algebraic and category- theoretic vocabulary useful in functional programming
  2. 3.

    Type classes in Scala • Scala has the features needed

    to encode type classes • traits for “classes,” implicits for instances, and subtyping for specifying relationships • Used in popular open source libraries like Scalaz, Cats, Scodec, Shapeless, Argonaut, Circe, Specs2, Algebra/ Algebird/Spire, FS2…
  3. 4.

    Type classes in Scala trait Functor[F[_]] { def map[A, B](fa:

    F[A])(f: A => B): F[B] } implicit val listFunctor: Functor[List] = new Functor[List] { def map[A, B](fa: List[A])(f: A => B): List[B] = fa match { case Nil => Nil case h :: t => f(h) :: map(f)(t) } } def void[F[_]: Functor, A](fa: F[A]): F[Unit] = implicitly[Functor[F]].map(fa)(_ => ())
  4. 5.

    Type classes in Scala trait Monad[F[_]] { def flatMap[A, B](fa:

    F[A])(f: A => F[B]): F[B] def pure[A](a: A): F[A] }
  5. 6.

    Type classes in Scala trait Monad[F[_]] { def flatMap[A, B](fa:

    F[A])(f: A => F[B]): F[B] def pure[A](a: A): F[A] def map[A, B](fa: F[A])(f: A => B): F[B] = flatMap(fa)(a => pure(f(a)) }
  6. 7.

    Type classes in Scala trait Monad[F[_]] extends Functor[F] { def

    flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] def pure[A](a: A): F[A] def map[A, B](fa: F[A])(f: A => B): F[B] = flatMap(fa)(a => pure(f(a)) } def needMonad[F[_]: Monad, A](fa: F[A]): F[Unit] = .. void(fa) ..
  7. 8.

    Type classes in Scala trait Monad[F[_]] extends Functor[F] { ..

    } trait Traverse[F[_]] extends Functor[F] { .. } def traverseAndMonad[F[_]: Monad: Traverse, A] (fa: F[A]): F[Unit] = .. void(fa) .. error: ambiguous implicit values: both value evidence$2 of type Traverse[F] and value evidence$1 of type Monad[F] match expected type Functor[F] void(fa) ^
  8. 11.

    coherency: every different valid typing derivation for a program leads

    to a resulting program that has the same dynamic semantics
  9. 13.

    Option 1. Assume type class coherency and solve the problem

    of guiding the resolver up the tree. Option 2. Make the compiler/resolver type class- aware.
  10. 14.

    Option 1: The Scato encoding • The main problem with

    the subtyped implicits encoding is subtyping • Subtyping allows us to treat subclasses (e.g. Monad) as superclasses (e.g. Functor) • What if we replaced subtyping with implicit conversions? • We can still treat subclasses as superclasses • Unlike subtyping, implicit conversions can be prioritized
  11. 15.

    Option 1: The Scato encoding trait Functor [F[_]] { ..

    } trait Monad [F[_]] { def functor: Functor[F] .. } trait Traverse[F[_]] { def functor: Functor[F] .. } trait Conversions1 { implicit def m2f[F[_]: Monad]: Functor[F] = implicitly[Monad[F]].functor } trait Conversions0 extends Conversions1 { implicit def t2f[F[_]: Traverse]: Functor[F] = implicitly[Traverse[F]].functor } object Prelude extends Conversions0
  12. 19.

    Option 2: Type class-aware Scala • Introduce a Coherent marker

    trait that changes behavior of the implicit resolver • Need to make sure choice of path cannot be detected at runtime • Object#{equals, hashCode} can violate this • Introduce a parametric Any?
  13. 20.

    EOF