Как использовать бесплатную монаду с Future[M[_]]
Я реализовал простой язык для процесса 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
было бы также полезно.
Я делаю это неправильно? Для этого есть какой-то монадный трансформатор?
1 ответ
Абстрактный тип данных Ops
определяет алгебру для выборки и хранения нескольких Record
s. Он описывает две операции, но это также единственное, что должна делать алгебра. Как на самом деле выполняются операции, не должно иметь никакого значения для Fetch
а также Store
единственная полезная вещь, которую вы ожидаете, это соответственно List[Record]
и List[Response]
,
Делая ожидаемый тип результата Fetch
а также Store
Future[List[Record]]]
Вы ограничиваете возможности интерпретации этой алгебры. Возможно, в ваших тестах вы не хотите асинхронно подключаться к веб-сервису или базе данных и просто хотите протестировать с помощью 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
с? Это работа нашего переводчика:
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)