2016-12-05 5 views
1

ich mich oft in einem Szenario, wo ich eine Schnittstelle wie so definiert haben:generieren Adapter für höhere kinded Schnittstellen

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

ich dann ein paar verschiedene Implementierungen jeweils auf dem Typ Höhere Kinded getippt schreiben, das macht am meisten Sinn für diese bestimmte Implementierung:

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

Alle Implementierungen sind perfekt gültig, alle ihre Ergebnisse in einem bestimmten höheren Kinded Typ gewickelt zurückzukehren.

ich oft kommen dann einige Business-Logik zu schreiben, lassen Sie uns sagen, dass in dem Block der Logik mit denen ich arbeite Future als Kontext verwendet, könnte ich so etwas schreiben:

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 

Der obige Code würde wirklich gut mit FooImpl2 funktionieren, aber die anderen Implementierungen nicht direkt einbetten. In diesem Szenario aufzuwickeln ich immer schreiben einfache Adapter:

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

Schreiben Adapter fein, aber es erfordert eine Menge vorformulierten, vor allem für Schnittstellen mit vielen Funktionen; Darüber hinaus erhält jede Methode genau die gleiche Anpassungsbehandlung für jede Methode. Was ich wirklich tun möchte, ist lift eine Adapterimplementierung aus einer bestehenden Implementierung, nur einmal in Adaptionsmechanismus angeben.

Ich glaube, ich möchte in der Lage sein, so etwas zu schreiben:

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

So konnte ich es wie so verwenden:

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

Die Frage ist: Wie kann ich die generateAdapterFn Funktion schreiben ?

Ich bin mir nicht sicher, wie ich das lösen soll oder ob es andere gängige Muster oder Lösungen für mein Problem gibt. Ich vermute, dass, um die generateAdapterFn Funktion zu schreiben, ich wünschte, ich würde ein Makro schreiben müssen? Wenn ja, wie könnte das geschehen?

Antwort

1

Was Sie suchen, ist eine natürliche Umwandlung von X zu Y (was Sie nannten). In Cats heißt FunctionK (mit dem beliebten Typ Alias ​​~>).

Sie könnten eine natürliche Transformation zwischen Option definieren und Future als:

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

Mit dem kind projector compiler plugin diese prägnanter geschrieben werden als:

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

Ihre generateAdapter Funktion dann wie folgt aussehen könnte:

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

Sie sollten dann in der Lage sein, ein FooInterface[Future]] wie zu erstellen:

val fooFuture = generateAdapter(FooImpl1)(opt2fut) 

Unrelated, Sie interessiert sein könnte etwas über die free monad zu lesen, die ähnliche Probleme wie die lösen verwendet wird Sie jetzt konfrontiert sind.

+0

Ehrfürchtig, das ist wirklich hilfreich, es gibt viel formaler, was passiert. In Szenarien, in denen es viele verschiedene Implementierungen einer gegebenen Schnittstelle gibt, wird das generateAdapter fn das Schreiben einer großen Menge von Vorsätzen speichern. – sungiant

+0

Vielleicht gibt es hier zwei Fragen, von denen die erste gut beantwortet wurde, aber daraus folgt, dass der Code, der beim Definieren der generateAdaptor-Funktion geschrieben wird, eher präskriptiv ist, dies gilt insbesondere für eine Schnittstelle, die viel mehr Funktionen hat als die im Beispiel. Gibt es eine Möglichkeit, dies nicht direkt zu schreiben und stattdessen die Implementierung zu generieren? Vielleicht Reflektion ... – sungiant

1

Den Code so lange wie möglich polymorph halten. Statt

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

Schreib

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 

Auf diese Weise vermeiden Sie Adapter für FooInterface insgesamt und nur verwandeln den Endwert (über eine natürliche Transformation (siehe Peter Neyens' Antwort) oder auch ganz einfach direkt schreiben).

-1

Erweiterung auf Peter Neyen Antwort (was ich als richtig markiert haben, wie es der wichtigste Teil meiner Frage beantwortet), hier ist ein Proof of Concept, wie Sie den Adapter zur Laufzeit mit Reflexion erzeugen:

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

Idealerweise könnte diese Funktion auch auf T[_] geschrieben werden, wobei T der Typ der Schnittstelle ist, so dass die Funktion zur Generierung von Adaptern für höherwertige Schnittstellen zur Laufzeit verwendet werden könnte.

Etwas wie:

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

Nicht wirklich sicher, ob das wäre, wie es allerdings zu schreiben ...

Ich denke, die ideale Lösung wäre, eine Compiler-Plugin zu haben, die den Code erzeugt in Peter Neyens Lösung, Vermeidung von Reflexionen und Vermeidung von Standardbildung.

+0

Wenn jemand eine bessere, sicherere Art und Weise weiß, den obigen Reflektionscode zu schreiben, speziell wenn es darum geht, mit höherstufigen Runtime-Typen umzugehen, würde ich gerne sehen, wie ich damit richtig umgehen soll. – sungiant