The Ops abstract data type defines an algebra for retrieving and storing multiple Record s. He describes two operations, but this is also the only thing algebra should do. How the operations are actually performed doesn't matter at all for Fetch and the Store , the only useful thing you expect is List[Record] and List[Response] , respectively.
By making the expected result type Fetch and Store a Future[List[Record]]] , you will limit the interpretation of this algebra. You may not want to connect asynchronously to a web service or database in your tests and just want to test using Map[Int, Result] or Vector[Result] , but now you need to return Future , which makes the tests more than they can be .
But saying that you do not need ETL[Future[List[Record]]] does not solve your question: you use asynchronous libraries, and you probably want to return some Future .
Starting from your first implementation:
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)
But now we still don't have Future ? That the work of our translator:
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, ... } }
With this interpreter (where, of course, you replace Future.successful(...) with something more useful), we can get our 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)
But we can still create another interpreter that does not 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)