Folding a list of different types using Shapeless

2020-05-21 05:36发布

问题:

As I known, shapeless provides the HList (Heterogenous list) type which can include multiple types.

Is it possible to fold HList? for example,

// ref - Composable application architecture with reasonably priced monad
// code - https://github.com/stew/reasonably-priced/blob/master/src/main/scala/reasonable/App.scala

import scalaz.{Coproduct, Free, Id, NaturalTransformation}

def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): ({type cp[α] = Coproduct[F,G,α]})#cp ~> H =
  new NaturalTransformation[({type cp[α] = Coproduct[F,G,α]})#cp,H] {
    def apply[A](fa: Coproduct[F,G,A]): H[A] = fa.run match {
      case -\/(ff) ⇒ f(ff)
      case \/-(gg) ⇒ g(gg)
    }
  }

type Language0[A] = Coproduct[InteractOp, AuthOp, A]
type Language[A] = Coproduct[LogOp, Language0, A]

val interpreter0: Language0 ~> Id = or(InteractInterpreter, AuthInterpreter)
val interpreter: Language ~> Id = or(LogInterpreter, interpreter0)


// What if we have `combine` function which folds HList 
val interpreters: Language ~> Id = combine(InteractInterpreter :: AuthInterpreter :: LoginInterpreter :: HNil)

Even, can I simplify generation of Langauge?

type Language0[A] = Coproduct[InteractOp, AuthOp, A]
type Language[A] = Coproduct[LogOp, Language0, A]

// What if we can create `Language` in one line
type Language[A] = GenCoproduct[InteractOp, AuthOp, LogOp, A]

回答1:

For the sake of a complete working example, suppose we've got some simple algebras:

sealed trait AuthOp[A]
case class Login(user: String, pass: String) extends AuthOp[Option[String]]
case class HasPermission(user: String, access: String) extends AuthOp[Boolean]

sealed trait InteractOp[A]
case class Ask(prompt: String) extends InteractOp[String]
case class Tell(msg: String) extends InteractOp[Unit]

sealed trait LogOp[A]
case class Record(msg: String) extends LogOp[Unit]

And some (pointless but compile-able) interpreters:

import scalaz.~>, scalaz.Id.Id

val AuthInterpreter: AuthOp ~> Id = new (AuthOp ~> Id) {
  def apply[A](op: AuthOp[A]): A = op match {
    case Login("foo", "bar") => Some("foo")
    case Login(_, _) => None
    case HasPermission("foo", "any") => true
    case HasPermission(_, _) => false
  }
}

val InteractInterpreter: InteractOp ~> Id = new (InteractOp ~> Id) {
  def apply[A](op: InteractOp[A]): A = op match {
    case Ask(p) => p
    case Tell(_) => ()
  }
}

val LogInterpreter: LogOp ~> Id = new (LogOp ~> Id) {
  def apply[A](op: LogOp[A]): A = op match {
    case Record(_) => ()
  }
}

At this point you should be able to fold over an HList of interpreters like this:

import scalaz.Coproduct
import shapeless.Poly2

object combine extends Poly2 {
  implicit def or[F[_], G[_], H[_]]: Case.Aux[
    F ~> H,
    G ~> H,
    ({ type L[x] = Coproduct[F, G, x] })#L ~> H
  ] = at((f, g) =>
    new (({ type L[x] = Coproduct[F, G, x] })#L ~> H) {
      def apply[A](fa: Coproduct[F, G, A]): H[A] = fa.run.fold(f, g)
    }
  )
}

But that doesn't work for reasons that seem to have something to do with type inference. It's not too hard to write a custom type class, though:

import scalaz.Coproduct
import shapeless.{ DepFn1, HList, HNil, :: }

trait Interpreters[L <: HList] extends DepFn1[L]

object Interpreters {
  type Aux[L <: HList, Out0] = Interpreters[L] { type Out = Out0 }

  implicit def interpreters0[F[_], H[_]]: Aux[(F ~> H) :: HNil, F ~> H] =
    new Interpreters[(F ~> H) :: HNil] {
      type Out = F ~> H
      def apply(in: (F ~> H) :: HNil): F ~> H = in.head
    }

  implicit def interpreters1[F[_], G[_], H[_], T <: HList](implicit
    ti: Aux[T, G ~> H]
  ): Aux[(F ~> H) :: T, ({ type L[x] = Coproduct[F, G, x] })#L ~> H] =
    new Interpreters[(F ~> H) :: T] {
      type Out = ({ type L[x] = Coproduct[F, G, x] })#L ~> H
      def apply(
        in: (F ~> H) :: T
      ): ({ type L[x] = Coproduct[F, G, x] })#L ~> H =
        new (({ type L[x] = Coproduct[F, G, x] })#L ~> H) {
          def apply[A](fa: Coproduct[F, G, A]): H[A] =
            fa.run.fold(in.head, ti(in.tail))
        }
    }
}

And then you can write your combine:

def combine[L <: HList](l: L)(implicit is: Interpreters[L]): is.Out = is(l)

And use it:

type Language0[A] = Coproduct[InteractOp, AuthOp, A]
type Language[A] = Coproduct[LogOp, Language0, A]

val interpreter: Language ~> Id =
  combine(LogInterpreter :: InteractInterpreter :: AuthInterpreter :: HNil)

You might be able to get the the Poly2 version working, but this type class would probably be straightforward enough for me. Unfortunately you're not going to be able to simplify the definition of the Language type alias in the way you want, though.