Как использовать свободную монаду с будущим[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
, единственное полезное, что вы ожидаете, - это соответственно aList[Record]
и AList[Response]
.Fetch
иStore
AFuture[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
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)