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