Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Adelbert Chang

October 22, 2017
Tweet

More Decks by Adelbert Chang

Other Decks in Programming

Transcript

  1. The Limitations of Type Classes
    as Subtyped Implicits
    Adelbert Chang
    Scala Symposium 2017

    View Slide

  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

    View Slide

  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…

    View Slide

  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)(_ => ())

    View Slide

  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]
    }

    View Slide

  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))
    }

    View Slide

  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)
    ..

    View Slide

  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)
    ^

    View Slide

  9. The subtyped implicits encoding of type
    classes can fail whenever the type class
    hierarchy branches.

    View Slide

  10. Source: https://github.com/tpolecat/cats-infographic under CC BY-SA 4.0

    View Slide

  11. coherency: every different valid typing
    derivation for a program leads to a
    resulting program that has the same
    dynamic semantics

    View Slide

  12. Eq[A]
    Ord[A] Eq[List[A]]
    Ord[List[A]]

    View Slide

  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.

    View Slide

  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

    View Slide

  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

    View Slide

  16. Option 1: The Scato encoding
    import Prelude._
    def resolves[F[_]: Monad: Traverse] =
    implicitly[Functor[F]]

    View Slide

  17. Option 2: Type class-aware Scala

    View Slide

  18. Option 2: Type class-aware Scala

    View Slide

  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?

    View Slide

  20. EOF

    View Slide