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?
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
.