【问题标题】:Generating adaptors for higher-kinded interfaces为更高级的接口生成适配器
【发布时间】:2016-12-05 12:14:50
【问题描述】:

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

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] { ... }

所有实现都是完全有效的,都返回包装在特定更高种类类型中的结果。

然后我经常来写一些业务逻辑,假设在我正在使用的逻辑块中使用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 函数,我需要编写一个宏?如果可以,怎么做?

【问题讨论】:

    标签: scala scalaz scala-macros scala-cats


    【解决方案1】:

    您正在寻找的是从XY(您称之为X[?] =&gt; Y[?])的自然转换。在 Cats 中称为FunctionK(具有流行的类型别名~&gt;)。

    您可以将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 的内容,它用于解决与您现在面临的类似问题。

    【讨论】:

    • 太棒了,这真的很有帮助,它更正式地说明了正在发生的事情。在给定接口有许多不同实现的场景中,generateAdapter fn 将节省编写大量样板文件。
    • 这里可能有两个问题,第一个问题你已经很好地回答了,但是接下来,实际上定义 generateAdaptor 函数时编写的代码是相当规范的,尤其是对于一个比示例中具有更多功能的接口。有没有办法避免直接写这个,而是生成实现?也许是反思……
    【解决方案2】:

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

    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 的回答)或直接很容易地转换)。

    【讨论】:

      【解决方案3】:

      扩展 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 解决方案中的代码,避免反射和样板代码。

      【讨论】:

      • 如果有人知道编写上述反射代码的更好、更安全的方法,特别是在处理运行时更高种类的类型时,我很想看看如何正确处理它。跨度>
      猜你喜欢
      • 2011-02-06
      • 2011-12-07
      • 2018-04-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多