Эффективная служба не может найти неявный экземпляр монады, связанный с контекстом

Я еще не очень силен в концепции эффектов, поэтому некоторые из моих предположений могут быть совершенно неверными. Пожалуйста, исправьте меня всякий раз, когда вы видите такие случаи.

Я строю приложение (не с нуля, а скорее разрабатываю скелет) со скала-котами и котами-эффектами. Основной класс расширяет IOApp и запускает веб-сервер:

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 ()

}

Это первая встреча с типом F[_]. Контекстная привязка : ConcurrentEffect: Timer говорит, что где-то объявлено два экземпляра: ConcurrentEffect[F[_]] и Timer[F[_]], если я правильно понимаю.

Пропуская HTTP-уровень приложения, обработчик маршрута использует сервис, который я пытаюсь реализовать, с двумя разными вариантами — DummyService и LiveServiceDummy должен всегда возвращать постоянные (фиктивные) данные, в то время как Live один отправляет запрос REST и анализирует JSON. ответ на внутренние модели предметной области:

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]()

}

Все идет нормально. Единственная загадка для меня заключается в том, почему мы должны иметь это Applicative неявным.

Но теперь я пытаюсь реализовать службу Live, которая также будет использовать Cache (для ограничения запросов):

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))

  }

}

Здесь SelfRefreshingCache требует присутствия экземпляра Sync, иначе я получаю сообщение об ошибке, говорящее, что он не определен при попытке создать экземпляр Ref. Кроме того, чтобы иметь возможность использовать оператор state.get.map(_.get(key)) в классе SelfRefreshingCache, я должен использовать ограничение Monad, предположительно, чтобы сообщить Scala, что мой тип F[_] внутри Cache может быть flatMap-ped.

В моей службе Live я пытаюсь использовать эту службу следующим образом:

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]] = ???

}

В настоящее время я застрял в ошибке компиляции, говоря, что у меня нет экземпляра Monad[Nothing]. И здесь вся моя история Main переворачивается: если я понимаю всю концепцию ограничений типа (требующую определения имплицитов в области вызова метода), то тип F[_] должен распространяться с самого Main уровня вниз до моего уровня. Live и должно быть что-то вроде IO. А для IO определены методы map и flatMap. На уровне обслуживания Live refreshExchangeRatesCache выполняет вызов REST (используя http4s, но это не имеет значения) и также должен работать на чем-то вроде IO.

Прежде всего, верны ли мои предположения о границах контекста и распространении F[_] от класса Main? Могу ли я тогда скрыть тип IO на уровне обслуживания Live? Или как предоставить требуемый неявный экземпляр Monad?


person shybovycha    schedule 24.08.2019    source источник


Ответы (2)


Это первое знакомство с типом F[]. : ConcurrentEffect: Timer context-bound говорит, что где-то объявлены два экземпляра: ConcurrentEffect[F[]] и Timer[F[_]], если я правильно понимаю.

Чтобы быть точным, он должен быть объявлен внутри неявной области действия.

Единственная загадка для меня заключается в том, почему мы должны иметь неявный Applicative.

Вам нужно свидетельство Applicative[F], потому что ваш метод использует pure[F] для поднятия ExchangeRate на F, где pure определено в классе типов Applicative:

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

Кроме того, чтобы иметь возможность использовать оператор state.get.map(_.get(key)) в классе SelfRefreshingCache, я должен использовать ограничение Monad

Поскольку вы используете .map, а не .flatMap, достаточно будет потребовать экземпляр Functor, а не Monad для определения класса SelfRefreshingCache. Для объекта-компаньона вам понадобится Monad, чтобы flatMap.

Прежде всего, верны ли мои предположения о границах контекста и распространении F[_] из класса Main?

Да. Когда вы строите всю свою программу в Main и «заполняете» IO там, где требуется F[_], компилятор будет искать наличие всех неявных свидетельств, требуемых от IO в области видимости, учитывая, что вы зафиксировали требования для каждого вызова метода. используя границы контекста или простые неявные параметры.

Могу ли я затем скрыть тип ввода-вывода на уровне службы Live?

IO скрыто в вашем подходе, так как Live знает только "форму" типа, то есть F[_]. Требуя немедленного решения вашей проблемы, в предыдущем ответе говорилось, что вам нужно добавить F к вызову вашего метода, чтобы компилятор мог сделать вывод, какой тип вы хотели заполнить refreshExchangeRatesCache.

person Yuval Itzchakov    schedule 25.08.2019
comment
Спасибо за такой подробный ответ и за то, что направили на все мои вопросы! - person shybovycha; 26.08.2019

Добавьте информацию о типе, в данном случае F, в строку

val cache = SelfRefreshingCache.create[F, Currency, ExchangeRate](refreshExchangeRatesCache[F], timeout)
person Sarath K S    schedule 24.08.2019
comment
Ну, это решает непосредственную проблему, но поднимает несколько других - Scala не может найти неявный ConcurrentEffect[F]. Вот почему я спрашиваю, верны ли мои предположения о распространении типа F[_] и границах контекста. - person shybovycha; 25.08.2019
comment
Я не просмотрел весь ваш код. Но все просто: если вы вызываете метод, требующий выполнения определенных условий, в момент вызова вы должны сообщить компилятору Scala, что я позабочусь о том, чтобы F соответствовал этим требованиям. Это означает, что для приведенного выше кода, который он не должен выдавать, не удалось найти неявный ConcurrenEffectt[F], так как вызывающая сторона, здесь метод apply говорит, что F имеет значение ConcurrentEffect[F] - person Sarath K S; 25.08.2019