在 Scala 中使用 Shapeless 折叠不同类型的列表

Folding a list of different types using Shapeless in Scala

据我所知,shapeless 提供了 HList (Heterogenous 列表) 类型,可以包含多种类型。

是否可以折叠HList?例如,

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

甚至,我可以简化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]

为了一个完整的工作示例,假设我们有一些简单的代数:

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]

还有一些(无意义但可编译的)解释器:

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

在这一点上,你应该能够像这样折叠 HList 个口译员:

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

但由于似乎与类型推断有关的原因,这不起作用。编写自定义类型并不难 class,不过:

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

然后你可以写你的 combine:

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

并使用它:

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

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

您可能能够使 Poly2 版本正常工作,但这种类型 class 对我来说可能已经足够简单了。不幸的是,您无法按照您想要的方式简化 Language 类型别名的定义。