scalaz / scalaz

Principled Functional Programming in Scala
Other
4.67k stars 704 forks source link

[scalaz-deriving] Invariant parent of Divide/Apply and Decidable/Alternative #1481

Closed fommil closed 6 years ago

fommil commented 7 years ago

This was originally opened as a question about Divisible.conquer but morphed into the realisation that scalaz-deriving will need an invariant parent of Apply and Divide to support typeclass derivation.

I'll pick this up... I'll create a fresh hierarchy in the stalactite repo and ask for comments there but I'd very much like to get a bincompat version of this into 7.2

original ticket follows:


trait Divisible[F[_]] extends Divide[F] {
  def conquer[A]: F[A]
}

how can this ever work? In scala we'd typically need to take evidence for Divisible[A] in the conquer parameter, the current signature means that we must ignore the A.

In scalaz-deriving I'm probably going to base contravariant derivations on Divide (or some variant with labels)

// @xuwei-k (you touched it last)

fommil commented 7 years ago

This seems to be a goer... but I'm unhappy with it because it requires the typeclass author to consider four cases. Shapeless doesn't allow for massive divide/conquer and always folds left, so it only considers the case equivalent to case (Solo(fa), Label(fb, lb)) (this means it can never be as efficient as scalaz-deriving)

I need to top and tail this, but I am also investigating using type members to be able to dramatically simplify the approach.

  sealed trait L[F[_], A]
  final case class Label[F[_], A](fa: F[A], label: String) extends L[F, A]
  final case class Solo[F[_], A](fa: F[A])                 extends L[F, A]

  implicit val tcdShow: LazyDivisible[L[Show, ?]] =
    new LazyDivisible[L[Show, ?]] {
      def conquer[A]: L[Show, A] = Solo(Show.shows(_ => ""))
      def divide2[A, B, C](fa: => L[Show, A], fb: => L[Show, B])(
        f: C => (A, B)
      ): L[Show, C] = Solo(
        Show.shows { c =>
          val (a, b) = f(c)
          (fa, fb) match {
            case (Label(fa, la), Label(fb, lb)) =>
              s"$la=${fa.shows(a)},$lb=${fb.shows(b)}"
            case (Label(fa, la), Solo(fb)) =>
              s"$la=${fa.shows(a)},${fb.shows(b)}"
            case (Solo(fa), Label(fb, lb)) =>
              s"${fa.shows(a)},$lb=${fb.shows(b)}"
            case (Solo(fa), Solo(fb)) =>
              s"${fa.shows(a)},${fb.shows(b)}"
          }
        }
      )
    }

(yeah, I know it'd be more efficient with Cord and that I'm missing a F[C] => F[C] call at the end to wrap with Foo(...)... this is just a PoC)

fommil commented 7 years ago

ok... I think this is nearly beat. How about abstracting over arity with one of these bad boys?

  trait Field[F[_]] {
    type A
    def label: String
    def value: A
    def typeclass: F[A]
  }

  def shown[A](parts: IList[Field[Show]]): String =
    parts.map { applied =>
      applied.typeclass.shows(applied.value)
    }.intercalate(",")

all credit to @non

I'm pretty sure this allows us to abstract over arity for applyX and divideX, e.g.

  def divideX[C[_]: Foldable, Z](f: Z => C[Field[F]]): F[Z]
fommil commented 6 years ago

PR in #1565