如何在 Future[M[_]] 中使用免费的 monad
How to use the free monad with Future[M[_]]
我已经使用免费的 monad 为 ETL 过程实现了一种简单的语言。当使用 List
作为数据获取和存储的输入和输出时,一切正常。但是我正在使用异步库并使用 Future[List]
常见的导入和定义
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
也很有用。
我做错了吗?是否有某种 monad 转换器?
抽象数据类型 Ops
定义代数以 fetch 和 store 多个 Record
s。它描述了两个操作,但这也是代数应该做的唯一事情。这些操作是如何实际执行的,对 Fetch
和 Store
根本不重要,您期望的唯一有用的东西分别是 List[Record]
和 List[Response]
.
通过将 Fetch
和 Store
的预期结果类型设为 Future[List[Record]]]
,您可以限制解释此代数的可能性。也许在您的测试中,您不想异步连接到 Web 服务或数据库,只想使用 Map[Int, Result]
或 Vector[Result]
进行测试,但现在您需要 return Future
这使得测试比它们本来应该的更复杂。
但是说您不需要 ETL[Future[List[Record]]]
并不能解决您的问题:您正在使用异步库并且您可能想要 return 一些 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
?那是我们口译员的工作:
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)
但是我们仍然可以创建一个不 return 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)
我已经使用免费的 monad 为 ETL 过程实现了一种简单的语言。当使用 List
作为数据获取和存储的输入和输出时,一切正常。但是我正在使用异步库并使用 Future[List]
常见的导入和定义
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
也很有用。
我做错了吗?是否有某种 monad 转换器?
抽象数据类型 Ops
定义代数以 fetch 和 store 多个 Record
s。它描述了两个操作,但这也是代数应该做的唯一事情。这些操作是如何实际执行的,对 Fetch
和 Store
根本不重要,您期望的唯一有用的东西分别是 List[Record]
和 List[Response]
.
通过将 Fetch
和 Store
的预期结果类型设为 Future[List[Record]]]
,您可以限制解释此代数的可能性。也许在您的测试中,您不想异步连接到 Web 服务或数据库,只想使用 Map[Int, Result]
或 Vector[Result]
进行测试,但现在您需要 return Future
这使得测试比它们本来应该的更复杂。
但是说您不需要 ETL[Future[List[Record]]]
并不能解决您的问题:您正在使用异步库并且您可能想要 return 一些 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
?那是我们口译员的工作:
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)
但是我们仍然可以创建一个不 return 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)