扩展副产品的自然转化

Extend NaturalTransformation of Coproducts

我有

F ~> H
G ~> H

其中 ~>cats.NaturalTransformation

我可以构建一个

λ[A => F[A] :+: G[A] :+: CNil] ~> H

使用 kind-projector 语法提高可读性

这是我的做法

def or[G[_]](g: G ~> H): λ[A => F[A] :+: G[A] :+: CNil] ~> H =
  new (λ[A => F[A] :+: G[A] :+: CNil] ~> H) {
    def apply[A](fa: F[A] :+: G[A] :+: CNil): H[A] =
      (fa.select[F[A]], fa.select[G[A]]) match {
        case (Some(ff), None) => f(ff)
        case (None, Some(gg)) => g(gg)
        // this can't happen, due to the definition of Coproduct
        case _ => throw new Exception("Something is wrong")
  }
}

这是有效的,虽然我愿意接受建议,因为它看起来不太漂亮。

现在,如果我有

λ[A => F[A] :+: G[A] :+: CNil] ~> H
K ~> H

我应该也能构造一个

λ[A => F[A] :+: G[A] :+: K[A] :+: CNil] ~> H

这就是我卡住的地方。我尝试使用 shapeless 中的 ExtendRight,但我无法让它工作。这是我的尝试:

def or[F[_] <: Coproduct, G[_], H[_], FG[_] <: Coproduct](f: F ~> H, g: G ~> H)(
  implicit e: ExtendRight.Aux[F[_], G[_], FG[_]]
): FG ~> H = new (FG ~> H) {
  def apply[A](fg: FG[A])(implicit
    sf: Selector[FG[A], F[A]],
    sg: Selector[FG[A], G[A]]
  ): H[A] =
    (fg.select[F[A]], fg.select[G[A]]) match {
      case (Some(ff), None) => f(ff)
      case (None, Some(gg)) => g(gg)
      // this can't happen, due to the definition of Coproduct
      case _ => throw new Exception("Something is wrong")
    }

}

但是编译器找不到 ExtendRight 参数的隐含证据。

这是一个可以玩的 MWE

import shapeless._
import shapeless.ops.coproduct._
import cats.~>

object Bar {
  val optionToList = new (Option ~> List) {
    def apply[A](x: Option[A]): List[A] = x match {
      case None => Nil
      case Some(a) => List(a)
    }
  }

  val idToList = new (Id ~> List) {
    def apply[A](x: Id[A]): List[A] = List(x)
  }

  val tryToList = new (scala.util.Try ~> List) {
    def apply[A](x: scala.util.Try[A]): List[A] = x match {
      case scala.util.Failure(_) => Nil
      case scala.util.Success(a) => List(a)
    }
  }

  type OI[A] = Option[A] :+: Id[A] :+: CNil
  val optionAndId: OI ~> List = Foo.or(optionToList, idToList)
  val all = Foo.or2(optionAndId, tryToList)

}

object Foo {
  def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): λ[A => F[A] :+: G[A] :+: CNil] ~> H =
    new (λ[A => F[A] :+: G[A] :+: CNil] ~> H) {
      def apply[A](fa: F[A] :+: G[A] :+: CNil): H[A] =
        (fa.select[F[A]], fa.select[G[A]]) match {
          case (Some(ff), None) => f(ff)
          case (None, Some(gg)) => g(gg)
          // this can't happen, due to the definition of Coproduct
          case _ => throw new Exception("Something is wrong, most likely in the type system")
        }
    }

  def or2[F[_] <: Coproduct, G[_], H[_], FG[_] <: Coproduct](f: F ~> H, g: G ~> H)(implicit
    e: ExtendRight.Aux[F[_], G[_], FG[_]]
    ): FG ~> H = new (FG ~> H) {
      def apply[A](fg: FG[A])(implicit
        sf: Selector[FG[A], F[A]],
        sg: Selector[FG[A], G[A]]
      ): H[A] =
        (fg.select[F[A]], fg.select[G[A]]) match {
          case (Some(ff), None) => f(ff)
          case (None, Some(gg)) => g(gg)
          // this can't happen, due to the definition of Coproduct
          case _ => throw new Exception("Something is wrong, most likely in the type system")
        }
  }
}

抱歉,前几天我没有时间发布这个,但我认为它可以满足您的需求。诀窍是以您甚至不需要选择器的方式安排事物。

import shapeless._
import shapeless.ops.coproduct._
import cats.~>

def or[F[_], G[_], H[_]](
  f: F ~> H,
  g: G ~> H
): ({ type L[x] = F[x] :+: G[x] :+: CNil })#L ~> H =
  new (({ type L[x] = F[x] :+: G[x] :+: CNil })#L ~> H) {
    object fg extends Poly1 {
      implicit def atF[A]: Case.Aux[F[A], H[A]] = at(f(_))
      implicit def atG[A]: Case.Aux[G[A], H[A]] = at(g(_))
    }

    def apply[A](c: F[A] :+: G[A] :+: CNil): H[A] = c.fold(fg)
  }

def or2[F[_], G[_] <: Coproduct, H[_]](
  f: F ~> H,
  g: G ~> H
): ({ type L[x] = F[x] :+: G[x] })#L ~> H =
  new (({ type L[x] = F[x] :+: G[x] })#L ~> H) {
    def apply[A](c: F[A] :+: G[A]): H[A] = c match {
      case Inl(fa) => f(fa)
      case Inr(ga) => g(ga)
    }
  }

(请注意,我在 or 中使用了 Poly1 以避免需要处理 CNil 异常情况。)

现在你可以这样写了:

val optionToList = new (Option ~> List) {
  def apply[A](x: Option[A]): List[A] = x.fold[List[A]](Nil)(List(_))
}

val idToList = new (Id ~> List) {
  def apply[A](x: Id[A]): List[A] = List(x)
}

val tryToList = new (scala.util.Try ~> List) {
  def apply[A](x: scala.util.Try[A]): List[A] = x match {
    case scala.util.Failure(_) => Nil
    case scala.util.Success(a) => List(a)
  }
}

然后:

scala> type OI[A] = Option[A] :+: Id[A] :+: CNil
defined type alias OI

scala> val optionAndId: OI ~> List = or(optionToList, idToList)
optionAndId: cats.~>[OI,List] = $anon@55224c4a

scala> val all = or2(tryToList, optionAndId)
all: cats.~>[[x]shapeless.:+:[scala.util.Try[x],OI[x]],List] = $anon@536a993

scala> all(Inl(scala.util.Try('foo)))
res8: List[Symbol] = List('foo)

scala> all(Inr(Inl(Option('foo))))
res9: List[Symbol] = List('foo)

scala> all(Inr(Inr(Inl('foo))))
res10: List[Symbol] = List('foo)

(如果你不介意写出类型,当然也可以使用 Coproduct[...](Option('foo)) 等。)