为更高种类的接口生成适配器

Generating adaptors for higher-kinded interfaces

我经常发现自己在这样的场景中定义了一个接口:

trait FooInterface [T[_]] {
  def barA (): T[Int]
  def barB (): T[Int]
  def barC (): T[Int]
}

然后我编写了几个不同的实现,每个实现都键入了对特定实现最有意义的 Higher Kinded Type:

object FooImpl1 extends FooInterface[Option] { ... }
object FooImpl2 extends FooInterface[Future] { ... }
object FooImpl3 extends FooInterface[({type X[Y] = ReaderT[Future, Database, Y]})#X] { ... }

所有实现都是完全有效的,所有 return 它们的结果都包含在特定的高级类型中。

然后我经常来写一些业务逻辑,假设我正在使用的逻辑块使用 Future 作为上下文,我可能会写这样的东西:

val foo: FooInterface[Future] = ???

def fn (): Future[Int] = Future { 42 }

val result: Future[Int] = for {
  x <- foo.barA ()
  y <- foo.barB ()
  z <- foo.barC ()
  w <- fn ()
} yield x + y + z + w

以上代码与 FooImpl2 配合使用效果非常好,但其他实现不会直接插入。在这种情况下,我总是会编写简单的适配器:

object FooImpl1Adapter extends FooInterface[Future] {
  val t = new Exception ("Foo impl 1 failed.")
  def barA (): Future[Int] = FooImpl1.barA () match {
    case Some (num) => Future.successful (num)
    case None => Future.failed (t)
  }
  def barB (): Future[Int] = FooImpl1.barB () match {
    case Some (num) => Future.successful (num)
    case None => Future.failed (t)
  }
  def barC (): Future[Int] = FooImpl1.barC () match {
    case Some (num) => Future.successful (num)
    case None => Future.failed (t)
  }
}

case class FooImpl3Adapter (db: Database) extends FooInterface[Future] {
  def barA (): Future[Int] = FooImpl3.barA ().run (db)
  def barB (): Future[Int] = FooImpl3.barB ().run (db)
  def barC (): Future[Int] = FooImpl3.barC ().run (db)
}

写适配器很好,但是它涉及很多样板,尤其是对于具有很多功能的接口;更重要的是,每种方法对每种方法都得到完全相同的适配处理。我真正想做的是 lift 来自现有实现的适配器实现,只在适配机制中指定一次。

我想我想写这样的东西:

def generateAdapterFn[X[_], Y[_]] (implx: FooInterface[X])(f: X[?] => Y[?]): FooInterface[Y] = ???

所以我可以这样使用它:

val fooImpl1Adapter: FooInterface[Future] = generateAdapterFn [?, Future] () { z => z match {
  case Some (obj) => Future.successful (obj)
  case None => Future.failed (t)
}}

问题是:如何编写 generateAdapterFn 函数?

我不太确定如何解决这个问题,或者我的问题是否有其他常见模式或解决方案。我怀疑要编写我想要的 generateAdapterFn 函数,我需要编写一个宏吗?如果可以,那该怎么做?

您正在寻找的是从 XY 的自然转换(您所谓的 X[?] => Y[?])。在 Cats 中称为 FunctionK(使用流行的类型别名 ~>)。

您可以将 OptionFuture 之间的自然转换定义为:

import cats.arrow.FunctionK
import scala.concurrent.Future

val option2future = new FunctionK[Option, Future] {
  def apply[A](opt: Option[A]): Future[A] = opt match {
    case Some(obj) => Future.succesful(obj)
    case None      => Future.failed(new Exception("none")) // t ??
  }
}

有了 kind projector compiler plugin 这可以写得更简洁:

val opt2fut = λ[FunctionK[Option, Future]]{
  case Some(obj) => Future.succesful(obj)
  case None      => Future.failed(new Exception("none")) // t ??
}

您的 generateAdapter 函数可能如下所示:

import cats.~>

def generateAdapter[X[_], Y[_]](implx: FooInterface[X])(f: X ~> Y): FooInterface[Y] =
  new FooInterface[Y] {
    def barA: Y[Int] = f(implx.barA)
    def barB: Y[Int] = f(implx.barB)
    def barC: Y[Int] = f(implx.barC)
  }

然后您应该能够创建一个 FooInterface[Future]] 作为 :

val fooFuture = generateAdapter(FooImpl1)(opt2fut)

无关,您可能有兴趣阅读有关 free monad 的内容,它用于解决与您现在面临的问题类似的问题。

尽可能长时间地保持代码的多态性。而不是

val result: Future[Int] = for {
  x <- foo.barA ()
  y <- foo.barB ()
  z <- foo.barC ()
  w <- fn ()
} yield x + y + z + w

import scalaz.Monad
import scalaz.syntax.monad._
// or
import cats.Monad
import cats.syntax.all._

def result[M[_]: Monad](foo: FooInterface[M], fn: () => M[Int]): M[Int] = for {
  x <- foo.barA ()
  y <- foo.barB ()
  z <- foo.barC ()
  w <- fn ()
} yield x + y + z + w

这样,您就可以完全避免为 FooInterface 编写适配器,而只转换最终值(通过自然转换(参见 Peter Neyens 的回答)或者也很容易直接转换)。

扩展 Peter Neyen 的回答(我已将其标记为正确,因为它回答了我问题的重要部分),这是关于如何在运行时使用反射生成适配器的概念证明:

def generateAdapterR[X[_], Y[_]](implx: FooInterface[X])(implicit
  f: X ~> Y): FooInterface[Y] = {
  import java.lang.reflect.{InvocationHandler, Method, Proxy}
  object ProxyInvocationHandler extends InvocationHandler {
    def invoke (
      proxy: scala.AnyRef,
      method: Method,
      args: Array[AnyRef]): AnyRef = {
      val fn = implx.getClass.getMethod (
        method.getName,
        method.getParameterTypes: _*)
      val x = fn.invoke (implx, args: _*)
      val fx = f.getClass.getMethods ()(0)
      fx.invoke (f, x)
    }
  }
  Proxy.newProxyInstance(
    classOf[FooInterface[Y]].getClassLoader,
    Array(classOf[FooInterface[Y]]),
    ProxyInvocationHandler
  ).asInstanceOf[FooInterface[Y]]
}

理想情况下,也可以在 T[_] 上键入此函数,T 是接口的类型,因此该函数可用于为任何更高类型的接口生成适配器运行时间。

类似于:

def genericGenerateAdapterR[T[_], X[_], Y[_]](implx: T[X[_]])(implicit
  f: X ~> Y): T[Y[_]] = ???

不太确定这是否是写法...

我认为理想的解决方案是使用编译器插件生成 Peter Neyen 解决方案中的代码,避免反射和样板文件。