2016-06-13 46 views
6

我已經爲使用免費monad的ETL過程實現了一種簡單的語言。當使用List作爲數據讀取和存儲的輸入和輸出時,一切工作正常。不過我使用異步圖書館和Future[List]如何在將來使用免費monad [M [_]]

常見的進口工作和定義

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] 

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

工作不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 
} 

工作如預期從...返回在flatMap/map是錯誤的 - 我沒有得到OpsF[Future]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] => 

我目前的解決方法是讓store接受Future[List[Record]],並讓翻譯地圖上的Future,但感覺笨拙。

該問題不是特定於List - Option也會有用。

我做錯了嗎?這是否有某種單體變壓器?

+0

這似乎是一個monad變壓器的典型模式,第一次看起來像是Haskell有一個'FreeT',但是無法在Scalaz或貓中找到它。 –

+3

從[7.2.0]開始,scalaz擁有'FreeT'(https://oss.sonatype.org/service/local/repositories/archive/org/scalaz/scalaz_2.11/7.2.0/scalaz_2.11-7.2的3.0 javadoc.jar /!/ index.html的#scalaz.FreeT)。 –

+1

我可以指向47度的圖書館嗎?它恰當地命名爲http://47deg.github.io/fetch/,它很快將成爲一個類型級的孵化器?請注意,我沒有爲47度工作,但它似乎已經爲您想要做的大部分工作提供瞭解決方案。 – wheaties

回答

7

的抽象數據類型Ops限定代數商店多個Record秒。它描述了兩種操作,但這也是代數應該做的唯一的事情。如何操作實際執行,不應該在所有FetchStore沒關係,你想到的唯一有用的東西是分別爲List[Record]List[Response]

通過使期望的結果類型爲FetchStore a Future[List[Record]]],可以限制如何解釋此代數的可能性。也許在你的測試,你不希望異步連接到Web服務或數據庫,只是想用Map[Int, Result]Vector[Result]測試,但現在你必須返回一個Future這使得測試更加複雜得多,他們可能。

但是說你不需要ETL[Future[List[Record]]]不能解決你的問題:你正在使用異步庫,你可能想要返回一些Future

與你的第一個實施開始:

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) 

但現在我們仍然沒有Future S'這是我們的口譯員的工作:

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, ... 
    } 
} 

有了這個解釋器(這裏當然你會取代Future.successful(...)的東西更有用),我們可以得到我們的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) 

,但我們仍然可以創建不同的解釋器不返回Future

import scala.collection.mutable.ListBuffer 
import cats.Id 

val interpretSync: EtlOp ~> Id = new (EtlOp ~> Id) { 
    val records: ListBuffer[Record] = ListBuffer() 
    def apply[A](op: EtlOp[A]): Id[A] = op match { 
    case Store(recs) => 
     records ++= recs 
     records.toList 
    case Fetch(offset, amount) => 
     records.drop(offset).take(amount).toList 
    } 
} 

val etlResponse: ETL[List[Response]] = 
    for { 
    _  <- store(List("a", "b", "c", "d")) 
    records <- fetch(1, 2) 
    resp <- store(records) 
    } yield resp 

val responses2: List[Response] = etlResponse.foldMap(interpretSync) 
// List(a, b, c, d, b, c) 
+0

啊,我明白了,這很有道理。看起來我一直在概念上將提升定義與解釋器的執行相混淆謝謝彼得。 – kostja

+0

@ peter-neyens當我們結合代數時,我們是否可以合併兩個解釋器 - 一個返回的Id和其他未來的未來? – arjunswaj

+0

我不確定你想以何種方式組合兩名口譯員。你總是可以解釋一個節目兩次,一次是「Id」,一次是「Future」。 –