4

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) extends Op[T]

    type OpF[A] = Free[Op, A]

    def get[T](name: String): OpF[T] = liftF[Op, T](Get[T](name))
}

One of the interpreters will be a wrapper around a third-party library, called Client here which its get method's signature is similar to:

class Client {
    def get[O <: Resource](name: String)
        (implicit f: Format[O], d: Definition[O]): Future[O] = ???
}

My question is how can I encode that requirement in my implementation?

class FutureOp extends (Op ~> Future) {
    val client = new Client()

    def apply[A](fa: Op[A]): Future[A] =
        fa match {
            case Get(name: String) =>
                client.get[A](name)
        }
}

I tried things like introducing bounds to my apply (like apply[A <: Resource : Format : Definition]) which didn't work.

I understand that FunctionK is to transform values of first-order-kinded types, but is there anyway in which I can encode the requirements of the type parameter?

I intend to use it like:

def run[F[_]: Monad, A](intp: Op ~> F, op: OpF[A]): F[A] = op.foldMap(intp)

val p: Op.OpF[Foo] = Op.get[Foo]("foo")

val i = new FutureOp()

run(i, d)
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
racetrack
  • 3,766
  • 30
  • 30
  • One idea would be to go first from `Op[A]` to a `({type L[A] = ReaderT[Future, (Format[A], Definition[A]), A]})#L` and then finally from that to a `Future[A]`. Not sure if it works, but it's what I would try. In other words, transform to a Reader to obtain the (implicit) params and just then call the `Client` method. – Ionuț G. Stan Mar 08 '18 at 12:21
  • I've tried it, but unfortunately it doesn't work due to the `Resource` subtype constraint: https://gist.github.com/igstan/3e90f8c704b42a97b9de9d66a67feb5d – Ionuț G. Stan Mar 08 '18 at 12:38
  • Can you please update the question with a sample of how you'd use the `Op` DSL? – Ionuț G. Stan Mar 08 '18 at 16:23
  • @ionuț-g-stan just did – racetrack Mar 08 '18 at 16:37

2 Answers2

4

(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

  1. Implicits are required for each type 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.
  2. There is a general strategy for gluing a natural transformation ~> 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:

  1. implicit Format and Definition
  2. <: Resource-type bound

I 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:

  1. First, I will summarize your question as I understand it.
  2. Then I will explain what to do with the implicits, ignoring the type bound.
  3. Then I will deal with the type bound, this time ignoring the implicits.
  4. Finally, I apply both strategies to your particular problem.

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 setup

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.

Dealing with implicits when defining interpreter for the Free monad

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 Definitions and Formats at the call site.

  • Therefore, all the Definitions and Formats 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)
  }
}

Type bounds in case classes defining the DSL-operations

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.

Applying both solution strategies to your problem

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.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • Thanks for your reply. What I don't understand is how changing the `get` in DSL fixes the issue of calling `client.get` with `[A]`? The problem is the `A`s boundaries aren't specified in `apply`. – racetrack Mar 08 '18 at 16:16
  • The problem is if I don't specify the type parameter in `client.get` I'll get `diverging implicit expansion` error. – racetrack Mar 08 '18 at 16:22
  • And if I just say `client.get[A]`, I'll get `type arguments [A] do not conform to method get's type parameter bounds [O <: Resource]` – racetrack Mar 08 '18 at 16:23
  • @racetrack Why would you want to not specify type parameter in `client.get`? Once the `Format` and `Definition` are stored in `Get[A <: Resource](name: String, fmt: Format[A], dfn: Definition[A])`, your interpreter `~>` can call `Client.get[A](name)(fmt, dfn)` explicitly. – Andrey Tyukin Mar 08 '18 at 16:24
  • well, the problem is that wouldn't work. It throws an error that it doesn't conform to method get's type parameter bounds `[O <: Resource]` – racetrack Mar 08 '18 at 16:41
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/166480/discussion-between-andrey-tyukin-and-racetrack). – Andrey Tyukin Mar 08 '18 at 16:42
  • @racetrack Added a much more detailed solution. The original answer already contained the solution to the main problem (compiler not being able to supply the implicits), but once this was solved, you got bogged down in a syntactical annoyance with the `<: Resource` bound. I've added two detailed solutions for both problems. Sry, didn't manage to finish it yesterday. The short answer didn't seem to help, the long answer required more effort and time... – Andrey Tyukin Mar 10 '18 at 19:13
1

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.

Ionuț G. Stan
  • 176,118
  • 18
  • 189
  • 202
  • Thanks for your answer, it's really helpful. Now I need a `Monad` instance for `Result` to be able to run it, right? Having a run method similar to: `def run[F[_]: Monad, A](intp: Op ~> F, op: OpF[A]): F[A] = op.foldMap(intp)` – racetrack Mar 08 '18 at 14:35
  • cats docs says “It has a Monad instance so long as the chosen F[_] does.”, and since the F is `Future`, I've imported `cats.instances.futures._` and `cats.implicits._` but still getting errors. – racetrack Mar 08 '18 at 14:44
  • @racetrack you probably need an implicit `ExecutionContext` in scope. – Ionuț G. Stan Mar 08 '18 at 14:44
  • @ionuț-g-stan I have one. – racetrack Mar 08 '18 at 14:47
  • @IonuțG.Stan Do I understand it correctly that this solution now requires that a `Format` and `Definition` must somehow be provided for *all* possible types `A`? This would restrict `Format`s and `Definition`s to things that are natural in `A`, but I'm not sure where the OP wants to obtain such highly regular and well behaved `Format`s that work for all types of objects. Somehow, it seems to me as if this solution says "assume that we *had* `Format`s and `Definition`s for all `A`...", simply pushing the problem with `Format` one level deeper? – Andrey Tyukin Mar 08 '18 at 15:53
  • @AndreyTyukin yes, you're right. In a way, all this follows from the fact that `Get` is generic in `A`, I think. So, as you said, `Get` probably has to carry the instances for `Format` and `Definition`. – Ionuț G. Stan Mar 08 '18 at 16:14
  • @racetrack I've tried definining a custom `Monad` instance for the `Result` type, but I failed. I don't think one can be defined. – Ionuț G. Stan Mar 08 '18 at 16:15
  • Thinking more about it, I realize that `Get` is akin to a function that takes a `String` and produces any type the user wants. That can't be done without additional knowledge (expressed by some typeclass constraint on that type `T` or similar). – Ionuț G. Stan Mar 08 '18 at 16:18
  • @ionuț-g-stan thanks, I'll undo the acceptance for now in order to get hopefully more attention to this question. – racetrack Mar 08 '18 at 16:18