Folding a list of different types using Shapeless in Scala

前端 未结 1 863
小鲜肉
小鲜肉 2021-02-03 14:05

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

Is it possible to fold HList

相关标签:
1条回答
  • 2021-02-03 15:01

    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.

    0 讨论(0)
提交回复
热议问题