Search code examples
scalafunctional-programmingmonadsscala-catsside-effects

Effectful service can not find context-bound implicit monad instance


I am not yet super strong with the concept of effects, so some of my assumptions might be completely wrong. Please correct me whenever you see such occurrences.

I am building an application (not from scratch, but rather developing the skeleton) with scala-cats and cats-effects. The main class extends IOApp and starts a web server:

object Main extends IOApp {

  override def run(args: List[String]): IO[ExitCode] =
    new Application[IO]
      .stream
      .compile
      .drain
      .as(ExitCode.Success)

}

class Application[F[_]: ConcurrentEffect: Timer] {

  def stream: Stream[F, Unit] =
    for {
      // ...
    } yield ()

}

This is the first encounter with the F[_] type. The : ConcurrentEffect: Timer context-bound says that there are two instances declared somewhere: ConcurrentEffect[F[_]] and Timer[F[_]] if I understand that correctly.

Skipping the HTTP layer of application, route handler uses the service I am trying to implement with two different variations - DummyService and LiveService - Dummy is supposed to always return the constant (dummy) data, whilst Live one sends a REST request and parses the JSON response to internal domain models:

trait CurrencyConverterAlgebra[F[_]] {

  def get(currency: Currency): F[Error Either ExchangeRate]

}

class DummyCurrencyConverter[F[_]: Applicative] extends CurrencyConverterAlgebra[F] {

  override def get(currency: Currency): F[Error Either ExchangeRate] =
    ExchangeRate(BigDecimal(100)).asRight[Error].pure[F]

}

object DummyCurrencyConverter {

  // factory method
  def apply[F[_]: Applicative]: CurrencyConverterAlgebra[F] = new DummyCurrencyConverter[F]()

}

So far so good. The only bit of mystery to me is why we have to have that Applicative implicit.

But now I try to implement the Live service which will also make use of the Cache (to throttle the requests):

trait Cache[F[_], K, V] {
  def get(key: K): F[Option[V]]

  def put(key: K, value: V): F[Unit]
}

private class SelfRefreshingCache[F[_]: Monad, K, V]
(state: Ref[F, Map[K, V]], refresher: Map[K, V] => F[Map[K, V]], timeout: FiniteDuration) extends Cache[F, K, V] {

  override def get(key: K): F[Option[V]] =
    state.get.map(_.get(key))

  override def put(key: K, value: V): F[Unit] =
    state.update(_.updated(key, value))

}

object SelfRefreshingCache {

  def create[F[_]: Monad: Sync, K, V]
  (refresher: Map[K, V] => F[Map[K, V]], timeout: FiniteDuration)
  (implicit timer: Timer[F]): F[Cache[F, K, V]] = {

    def refreshRoutine(state: Ref[F, Map[K, V]]): F[Unit] = {
      val process = state.get.flatMap(refresher).map(state.set)

      timer.sleep(timeout) >> process >> refreshRoutine(state)
    }

    Ref.of[F, Map[K, V]](Map.empty)
      .flatTap(refreshRoutine)
      .map(ref => new SelfRefreshingCache[F, K, V](ref, refresher, timeout))

  }

}

Here, SelfRefreshingCache requires Sync instance to be present - otherwise I am getting an error saying it is not defined when trying to construct a Ref instance. Also, in order to be able to use the state.get.map(_.get(key)) statement in the SelfRefreshingCache class, I have to use the Monad constraint, presumingly to tell Scala that my F[_] type inside Cache can be flatMap-ped.

In my Live service I am trying to use this service as follows:

class LiveCurrencyConverter[F[_]: Monad](cache: F[Cache[F, Currency, ExchangeRate]]) extends Algebra[F] {

  override def get(currency: Currency): F[Error Either ExchangeRate] =
    cache.flatMap(_.get(currency))
      .map(_.toRight(CanNotRetrieveFromCache()))

}

object LiveCurrencyConverter {

  def apply[F[_]: Timer: ConcurrentEffect]: Algebra[F] = {
    val timeout = Duration(30, TimeUnit.MINUTES)

    val cache = SelfRefreshingCache.create[F, Currency, ExchangeRate](refreshExchangeRatesCache, timeout)
    // ---> could not find implicit value for evidence parameter of type cats.Monad[Nothing]

    new LiveCurrencyConverter(cache)
  }

  private def refreshExchangeRatesCache[F[_]: Monad: ConcurrentEffect](existingRates: Map[Currency, ExchangeRate]): F[Map[Currency, ExchangeRate]] = ???

}

Currently, I am stuck at getting compilation error saying I don't have an instance of Monad[Nothing]. And this is where my whole story Main turns around: if I understand the whole concept behind type constraints (requiring implicits to be defined in the scope of a method call), then the F[_] type should be propagated from the very Main level down to my Live service and should be something like IO. And IO has both map and flatMap methods defined. On a Live service level, the refreshExchangeRatesCache makes a REST call (using the http4s, but that should not matter) and is supposed to run on something like IO as well.

First of all, are my assumptions about context boundaries and F[_] propagation from the Main class correct? Can I then hide the IO type on the Live service level? Or how do I provide the required Monad implicit instance?


Solution

  • This is the first encounter with the F[] type. The : ConcurrentEffect: Timer context-bound says that there are two instances declared somewhere: ConcurrentEffect[F[]] and Timer[F[_]] if I understand that correctly.

    To be specific, it has to be declared inside the implicit scope.

    The only bit of mystery to me is why we have to have that Applicative implicit.

    You need evidence of an Applicative[F] because your method uses pure[F] to lift ExchangeRate onto F, where pure is defined in the Applicative typeclass:

    ExchangeRate(BigDecimal(100)).asRight[Error].pure[F]
    

    Also, in order to be able to use the state.get.map(_.get(key)) statement in the SelfRefreshingCache class, I have to use the Monad constraint

    Since you're using .map and not .flatMap, it will suffice to require an instance of Functor and not Monad for the class definition of SelfRefreshingCache. For the companion object, you'll need a Monad in order to flatMap.

    First of all, are my assumptions about context boundaries and F[_] propagation from the Main class correct?

    Yes, they are. When you build your entire program in Main, and "fill in" IO where F[_] is required, the compiler will search for the existence of all the implicit evidence required from IO in scope, given that you've captured the requirements from each method invocation using context bounds or plain implicit parameters.

    Can I then hide the IO type on the Live service level?

    The IO is hidden in your approach, as Live only knows the "shape" of the type, i.e. F[_]. Requiring the immediate solution to your problem, the previous answer has stated you need to add F to your method call in order for the compiler to infer which type you meant to fill in refreshExchangeRatesCache.