Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defer could use some additional instances #4413

Closed
morgen-peschke opened this issue Mar 31, 2023 · 3 comments
Closed

Defer could use some additional instances #4413

morgen-peschke opened this issue Mar 31, 2023 · 3 comments

Comments

@morgen-peschke
Copy link
Contributor

Defer is really handy when writing typeclass instances for generic recursive ADTs, and unfortunately it's currently missing most of the instances that tend to be useful for this shape of data type.

I'd like to add Defer instances for the following typeclasses:

  • Eq
  • Order
  • Hash
  • Show

I'd also like to add Defer instances for these related typeclasses, mostly for completeness:

  • Equiv
  • Ordering
  • PartialOrder
  • PartialOrdering

As an example of how fiddly this can be, a simplified Test[A] has this shape:

sealed trait Test[A]
object Test {
  case class LessThan[A](a: A) extends Test[A]
  case class EqualTo[A](a: A) extends Test[A]
  case class GreaterThan[A](a: A) extends Test[A]
  case class Not[A](test: Test[A]) extends Test[A]
  case class Forall[A](tests: List[Test[A]]) extends Test[A]
  case class Exists[A](tests: List[Test[A]]) extends Test[A]

  implicit def show[A: Show]: Show[Test[A]] = ???
}

The sort of instance which would work for this if Test weren't generic looks like this:

 implicit def show[A: Show]: Show[Test[A]] = {
   Show.show {
     case LessThan(a)    => s"_ < ${a.show}"
     case EqualTo(a)     => s"_ = ${a.show}"
     case GreaterThan(a) => s"_ > ${a.show}"
     case Not(test)      => s"!(${test.show})"
     case Forall(tests)  => tests.mkString_("forall(", " && ", ")")
     case Exists(tests)  => tests.mkString_("exists(", " || ", ")")
   }
 }

scastie

Unfortunately, because it doesn't provide a stable implicit, it generates a new instance every time it recurses.

Implementing an anonymous subclass can avoid this, but only if an implicit self-reference is added:

  implicit def show[A: Show]: Show[Test[A]] =
    new Show[Test[A]] {
      implicit private val self: Show[Test[A]] = this

      def show(a: Test[A]): String = a match {
        case LessThan(a)    => s"_ < ${a.show}"
        case EqualTo(a)     => s"_ = ${a.show}"
        case GreaterThan(a) => s"_ > ${a.show}"
        case Not(test)      => s"!(${test.show})"
        case Forall(tests)  => tests.mkString_("forall(", " && ", ")")
        case Exists(tests)  => tests.mkString_("exists(", " || ", ")")
      }
    }

scastie

In comparison, Defer results in an idiomatic implementation that neatly avoids the easy errors of the other versions:

  implicit def show[A: Show]: Show[Test[A]] =
    Defer[Show].fix { implicit s =>
      Show.show {
        case LessThan(a)    => s"_ < ${a.show}"
        case EqualTo(a)     => s"_ = ${a.show}"
        case GreaterThan(a) => s"_ > ${a.show}"
        case Not(test)      => s"!(${test.show})"
        case Forall(tests)  => tests.mkString_("forall(", " && ", ")")
        case Exists(tests)  => tests.mkString_("exists(", " || ", ")")
      }
    }

scastie

@johnynek
Copy link
Contributor

johnynek commented Apr 1, 2023

This is a really nice observation!

@rossabaker
Copy link
Member

catsContravariantMonoidalForEq came up at work recently, and I was struggling to think of other times we provide instances for some other type class. It got me wondering whether there's some ancient taboo on this approach.

If nobody can remember why not, I think this is a splendid idea.

@johnynek
Copy link
Contributor

johnynek commented Jul 7, 2023

@rossabaker I think we have actually just not implemented all the type-classes on type-classes that we have. We should add them. Especially the first order type classes F[_] like Eq, Hash, Order, Monoid, ... those have a lot of typeclasses. The second order type-classes F[_[_]] have some as well, but they are more abstract and we probably haven't noticed all of them yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants