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
(My original answer contained the same idea, but apparently it did not provide enough implementation details. This time, I wrote a more detailed step-by-step guide with a discussion of each intermediate step. Every section contains a separate compilable code snippet.)
TL;DR
T
that occurs in get[T]
, therefore they must be inserted and stored when the DSL-program is constructed, not when it is executed. This solves the problem with the implicits.~>
from several restricted natural transformations trait RNT[R, F[_ <: R], G[_]]{ def apply[A <: R](x: F[A]): G[A] }
using pattern matching. This solves the problem with the A <: Resource
type bound. Details below.In your question, you have two separate problems:
Format
and Definition
<: Resource
-type boundI want to treat each of these two problems in isolation, and provide a reusable solution strategy for both. I will then apply both strategies to your problem.
My answer below is structured as follows:
Henceforth, I assume that you have scalaVersion
2.12.4
, the dependencies
libraryDependencies += "org.typelevel" %% "cats-core" % "1.0.1"
libraryDependencies += "org.typelevel" %% "cats-free" % "1.0.1"
and that you insert
import scala.language.higherKinds
where appropriate.
Note that the solution strategies are not specific to this particular scala version or the cats
library.
The goal of this section is to make sure that I'm solving the right problem, and also to provide very simple mock-up definitions
of Resource
, Format
, Client
etc., so that this answer is self-contained
and compilable.
I assume that you want to build a little domain specific language using the Free
monad.
Ideally, you would like to have a DSL that looks approximately like this (I've used the names DslOp
for the operations and Dsl
for the generated free monad):
import cats.free.Free
import cats.free.Free.liftF
sealed trait DslOp[A]
case class Get[A](name: String) extends DslOp[A]
type Dsl[A] = Free[DslOp, A]
def get[A](name: String): Dsl[A] = liftF[DslOp, A](Get[A](name))
It defines a single command get
that can get objects of type A
given a string
name.
Later, you want to interpret this DSL using a get
method provided by some Client
that you cannot modify:
import scala.concurrent.Future
trait Resource
trait Format[A <: Resource]
trait Definition[A <: Resource]
object Client {
def get[A <: Resource](name: String)
(implicit f: Format[A], d: Definition[A]): Future[A] = ???
}
Your problem is that the get
method of the Client
has a type bound, and that
it requires additional implicits.
Let's first pretend that the get
-method in client requires implicits, but
ignore the type bound for now:
import scala.concurrent.Future
trait Format[A]
trait Definition[A]
object Client {
def get[A](name: String)(implicit f: Format[A], d: Definition[A])
: Future[A] = ???
}
Before we write down the solution, let's briefly discuss why you cannot supply all
the necessary implicits when you are calling the apply
method in ~>
.
When passed to foldMap
, the apply
of FunctionK
is supposed
to be able to cope with arbitrarily long programs of type Dsl[X]
to produce Future[X]
.
Arbitrarily long programs of type Dsl[X]
can contain an unlimited number of
get[T1]
, ..., get[Tn]
commands for different types T1
, ..., Tn
.
For each of those T1
, ..., Tn
, you have to get a Format[T_i]
and Definition[T_i]
somewhere.
These implicit arguments must be supplied by the compiler.
When you interpret the entire program of type Dsl[X]
, only the type X
but not the types T1
, ..., Tn
are available,
so the compiler cannot insert all the necessary Definition
s and Format
s at the call site.
Therefore, all the Definition
s and Format
s must be supplied as implicit parameters to get[T_i]
when you are constructing the Dsl
-program, not when you are interpreting it.
The solution is to add Format[A]
and Definition[A]
as members to the Get[A]
case class,
and make the definition of get[A]
with lift[DslOp, A]
accept these two additional implicit
parameters:
import cats.free.Free
import cats.free.Free.liftF
import cats.~>
sealed trait DslOp[A]
case class Get[A](name: String, f: Format[A], d: Definition[A])
extends DslOp[A]
type Dsl[A] = Free[DslOp, A]
def get[A](name: String)(implicit f: Format[A], d: Definition[A])
: Dsl[A] = liftF[DslOp, A](Get[A](name, f, d))
Now, we can define the first approximation of the ~>
-interpreter, which at least
can cope with the implicits:
val clientInterpreter_1: (DslOp ~> Future) = new (DslOp ~> Future) {
def apply[A](op: DslOp[A]): Future[A] = op match {
case Get(name, f, d) => Client.get(name)(f, d)
}
}
Now, let's deal with the type bound in isolation. Suppose that your Client
doesn't need any implicits, but imposes an additional bound on A
:
import scala.concurrent.Future
trait Resource
object Client {
def get[A <: Resource](name: String): Future[A] = ???
}
If you tried to write down the clientInterpreter
in the same way as in the
previous example, you would notice that the type A
is too general, and that
you therefore cannot work with the contents of Get[A]
in Client.get
.
Instead, you have to find a scope where the additional type information A <: Resource
is not lost. One way to achieve it is to define an accept
method on Get
itself.
Instead of a completely general natural transformation ~>
, this accept
method will
be able to work with natural transformations with restricted domain.
Here is a trait to model that:
trait RestrictedNat[R, F[_ <: R], G[_]] {
def apply[A <: R](fa: F[A]): G[A]
}
It looks almost like ~>
, but with an additional A <: R
restriction. Now we
can define accept
in Get
:
import cats.free.Free
import cats.free.Free.liftF
import cats.~>
sealed trait DslOp[A]
case class Get[A <: Resource](name: String) extends DslOp[A] {
def accept[G[_]](f: RestrictedNat[Resource, Get, G]): G[A] = f(this)
}
type Dsl[A] = Free[DslOp, A]
def get[A <: Resource](name: String): Dsl[A] =
liftF[DslOp, A](Get[A](name))
and write down the second approximation of our interpreter, without any nasty type-casts:
val clientInterpreter_2: (DslOp ~> Future) = new (DslOp ~> Future) {
def apply[A](op: DslOp[A]): Future[A] = op match {
case g @ Get(name) => {
val f = new RestrictedNat[Resource, Get, Future] {
def apply[X <: Resource](g: Get[X]): Future[X] = Client.get(g.name)
}
g.accept(f)
}
}
}
This idea can be generalized to an arbitrary number of type constructors Get_1
, ...,
Get_N
, with type restrictions R1
, ..., RN
. The general idea corresponds to
the construction of a piecewise defined natural transformation from smaller
pieces that work only on certain subtypes.
Now we can combine the two general strategies into one solution for your concrete problem:
import scala.concurrent.Future
import cats.free.Free
import cats.free.Free.liftF
import cats.~>
// Client-definition with both obstacles: implicits + type bound
trait Resource
trait Format[A <: Resource]
trait Definition[A <: Resource]
object Client {
def get[A <: Resource](name: String)
(implicit fmt: Format[A], dfn: Definition[A])
: Future[A] = ???
}
// Solution:
trait RestrictedNat[R, F[_ <: R], G[_]] {
def apply[A <: R](fa: F[A]): G[A]
}
sealed trait DslOp[A]
case class Get[A <: Resource](
name: String,
fmt: Format[A],
dfn: Definition[A]
) extends DslOp[A] {
def accept[G[_]](f: RestrictedNat[Resource, Get, G]): G[A] = f(this)
}
type Dsl[A] = Free[DslOp, A]
def get[A <: Resource]
(name: String)
(implicit fmt: Format[A], dfn: Definition[A])
: Dsl[A] = liftF[DslOp, A](Get[A](name, fmt, dfn))
val clientInterpreter_3: (DslOp ~> Future) = new (DslOp ~> Future) {
def apply[A](op: DslOp[A]): Future[A] = op match {
case g: Get[A] => {
val f = new RestrictedNat[Resource, Get, Future] {
def apply[X <: Resource](g: Get[X]): Future[X] =
Client.get(g.name)(g.fmt, g.dfn)
}
g.accept(f)
}
}
}
Now, the clientInterpreter_3
can cope with both problems: the type-bound-problem is dealt with
by defining a RestrictedNat
for each case class that imposes an upper bound on its type arguments,
and the implicits-problem is solved by adding an implicit parameter list to DSL's get
-method.
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
.