I\'m using cats FreeMonad. Here\'s a simplified version of the algebra:
sealed trait Op[A]
object Op {
final case class Get[T](name: String
I think I've found a way to solve your problem by combining a ReaderT monad transformer with intersection types:
import scala.concurrent.Future
import cats.~>
import cats.data.ReaderT
import cats.free.Free
object FreeMonads {
sealed trait Op[A]
object Op {
final case class Get[T](name: String) extends Op[T]
type OpF[A] = Free[Op, A]
def get[T](name: String): OpF[T] = Free.liftF[Op, T](Get[T](name))
}
trait Resource
trait Format[A]
trait Definition[A]
trait Client {
def get[O <: Resource](name: String)
(implicit f: Format[O], d: Definition[O]): Future[O]
}
type Result[A] = ReaderT[
Future,
(Format[A with Resource], Definition[A with Resource]),
A,
]
class FutureOp(client: Client) extends (Op ~> Result) {
def apply[A](fa: Op[A]): Result[A] =
fa match {
case Op.Get(name: String) =>
ReaderT {
case (format, definition) =>
// The `Future[A]` type ascription makes Intellij IDEA's type
// checker accept the code.
client.get(name)(format, definition): Future[A]
}
}
}
}
The basic idea behind it is that you produce a Reader from your Op and that Reader receives the values that you can use for the implicit params. This solves the problem of type O having instances for Format and Definition.
The other problem is for O be a subtype of Resource. To solve this, we're just saying that the Format and Definition instances are not just instances of any A, but any A that also happens to be a Resource.
Let me know if you bump into problems when using FutureOp.