2016-06-13 21 views
6

Ich habe eine einfache Sprache für einen ETL-Prozess mit der freien Monade implementiert. Bei der Verwendung von List als Eingabe und Ausgabe sowohl zum Abrufen als auch zum Speichern von Daten funktioniert alles einwandfrei. Ich verwende jedoch Asynchron-Bibliotheken und die Arbeit mit Future[List]So verwenden Sie die freie Monade mit Future [M [_]]

gemeinsamen Ein- und Definitionen

import scala.concurrent.Future 
import scala.concurrent.ExecutionContext.Implicits.global 
import cats.free.Free 
import cats.free.Free._ 

sealed trait Ops[A] 
type OpsF[A] = Free[Ops, A] 

Arbeit mit List

case class Fetch(offset: Int, amount: Int) extends Ops[List[Record]] 
case class Store(recs: List[Record]) extends Ops[List[Response]] 

def fetch(offset: Int, amount: Int): OpsF[List[Record]] = 
    liftF[Ops, List[Record]](Fetch(offset, amount)) 
def store(recs: List[Record]): OpsF[List[Response]] = 
    liftF[Ops, List[Response]](Store(recs)) 

def simpleEtl(offset: Int, amount: Int): Free[Ops, List[Response]] = 
    fetch(offset, amount).flatMap(r => store(r)) 

funktionieren nicht mit Future[List]

case class Fetch(offset: Int, amount: Int) extends Ops[Future[List[Record]]] 
case class Store(recs: List[Record]) extends Ops[Future[List[Response]]] 

def fetch(offset: Int, amount: Int): OpsF[Future[List[Record]]] = 
    liftF[Ops, Future[List[Record]]](Fetch(offset, amount)) 
def store(recs: List[Record]): OpsF[Future[List[Response]]] = 
    liftF[Ops, Future[List[Response]]](Store(recs)) 

// explicit types in case I am misunderstanding more than I think 
def simpleEtl(offset: Int, amount: Int): Free[Ops, Future[List[Response]]] = 
fetch(offset, amount).flatMap { rf: Future[List[Record]] => 
    val getResponses: OpsF[Future[List[Response]]] = rf map { r: List[Record] => 
    store(r) 
    } 
    getResponses 
} 

wie erwartet, die Typ zurückgegeben von die flatMap/map ist falsch - ich bin nicht OpsF[Future] bekomme aber eine Future[OpsF]

Error:(34, 60) type mismatch; 
found : scala.concurrent.Future[OpsF[scala.concurrent.Future[List[Response]]]] 
(which expands to) scala.concurrent.Future[cats.free.Free[Ops,scala.concurrent.Future[List[String]]]] 
required: OpsF[scala.concurrent.Future[List[Response]]] 
(which expands to) cats.free.Free[Ops,scala.concurrent.Future[List[String]]] 
    val getResponses: OpsF[Future[List[Response]]] = rf map { r: List[Record] => 

meine aktuelle Problemumgehung ist store akzeptieren Future[List[Record]] und läßt die Interpreter Karte über die Future haben, aber es fühlt sich ungeschickt. Das Problem ist nicht spezifisch für List - z. Option wäre auch nützlich.

Mache ich es falsch? Gibt es dafür eine Art Monodentrafo?

+0

Dies scheint wie ein typisches Muster für einen Monade-Transformator, auf den ersten Blick scheint wie Haskell hat irgendwie ein 'FreeT', konnte es aber nicht in Scalaz oder Katzen finden. –

+3

scalaz hat 'FreeT' seit [7.2.0] (https://oss.sonatype.org/service/local/repositories/releases/archive/org/scalaz/scalaz_2.11/7.2.0/scalaz_2.11-7.2 .0-javadoc.jar /!/Index.html # scalaz.FreeT). –

+1

Kann ich Sie auf die 47-Grad-Bibliothek mit dem treffenden Namen http://47deg.github.io/fetch/ verweisen, die bald zu einem Typ-Level-Inkubator wird? Wohlgemerkt, ich arbeite nicht für 47 Grad, aber es sieht so aus, als ob es bereits eine Lösung für vieles von dem, was Sie tun wollen, hat. – wheaties

Antwort

7

Der abstrakte Datentyp Ops definiert eine Algebra zu holen und zu store mehr Record s. Es beschreibt zwei Operationen, aber das ist auch das einzige, was die Algebra tun sollte. Wie die Operationen tatsächlich ausgeführt werden, sollte Fetch und Store überhaupt nichts ausmachen, das einzige nützliche Ding, das Sie erwarten, ist ein List[Record] und ein List[Response].

Mit dem erwarteten Ergebnistyp Fetch und Store a Future[List[Record]]] begrenzen Sie die Möglichkeiten, wie diese Algebra zu interpretieren ist. Vielleicht möchten Sie in Ihren Tests keine asynchrone Verbindung mit einem Webservice oder einer Datenbank herstellen und nur mit einer Map[Int, Result] oder Vector[Result] testen, aber jetzt müssen Sie eine Future zurückgeben, die die Tests komplexer als sie sein könnten.

Aber sagen, dass Sie ETL[Future[List[Record]]] nicht benötigen, löst Ihre Frage nicht: Sie verwenden async Bibliotheken und Sie möchten wahrscheinlich einige Future zurückgeben.

Beginnend mit dem ersten Implementierung:

import scala.concurrent.Future 
import scala.concurrent.ExecutionContext.Implicits.global 
import cats.implicits._ 
import cats.free.Free 

type Record = String 
type Response = String 

sealed trait EtlOp[T] 
case class Fetch(offset: Int, amount: Int) extends EtlOp[List[Record]] 
case class Store(recs: List[Record]) extends EtlOp[List[Response]] 

type ETL[A] = Free[EtlOp, A] 

def fetch(offset: Int, amount: Int): ETL[List[Record]] = 
    Free.liftF(Fetch(offset, amount)) 
def store(recs: List[Record]): ETL[List[Response]] = 
    Free.liftF(Store(recs)) 

def fetchStore(offset: Int, amount: Int): ETL[List[Response]] = 
    fetch(offset, amount).flatMap(store) 

Aber jetzt haben wir noch keine Future s?Das ist die Aufgabe unserer Dolmetscher:

import cats.~> 

val interpretFutureDumb: EtlOp ~> Future = new (EtlOp ~> Future) { 
    def apply[A](op: EtlOp[A]): Future[A] = op match { 
    case Store(records) => 
     Future.successful(records.map(rec => s"Resp($rec)")) 
     // store in DB, send to webservice, ... 
    case Fetch(offset, amount) => 
     Future.successful(List.fill(amount)(offset.toString)) 
     // get from DB, from webservice, ... 
    } 
} 

Mit diesem Interpreter (wo natürlich würden Sie Future.successful(...) mit etwas nützlicher ersetzen) können wir unser erhalten Future[List[Response]]:

val responses: Future[List[Response]] = 
    fetchStore(1, 5).foldMap(interpretFutureDumb) 

val records: Future[List[Record]] = 
    fetch(2, 4).foldMap(interpretFutureDumb) 

responses.foreach(println) 
// List(Resp(1), Resp(1), Resp(1), Resp(1), Resp(1)) 
records.foreach(println) 
// List(2, 2, 2, 2) 

Aber wir können noch eine andere erstellen Interpreter, der keine Future:

zurückgibt
+0

Ah, ich verstehe, macht Sinn. Sieht so aus, als hätte ich konzeptionell die Lifting-Definitionen mit der Ausführung durch den Dolmetscher verwechselt. Danke, Peter. – kostja

+0

@ peter-neyens Wenn wir Algebra kombinieren, können wir zwei Dolmetscher kombinieren - einen wiederkehrenden Id und andere wiederkehrende Future? – arjunswaj

+0

Ich bin nicht sicher, auf welche Weise Sie zwei Dolmetscher kombinieren möchten. Sie können ein Programm immer zweimal interpretieren, einmal zu "Id" und einmal zu "Future". –