Стекирование монад Writer и OptionT

У меня есть следующий код:

override def getStandsByUser(email: String): Try[Seq[Stand]] =
  (for {
    user <- OptionT(userService.findOneByEmail(email)): Try[Option[User]]
    stands <- OptionT.liftF(standService.list()):[Try[List[Stand]]]
    filtered = stands.filter(stand => user.stands.contains(stand.id))
  } yield filtered).getOrElse(Seq())
}

Я хочу добавить ведение журнала на каждом этапе обработки, поэтому мне нужно ввести монаду писателя и стекировать ее с преобразователем монады OptionT. Не могли бы вы подсказать, как это сделать?


person Sergii Shevchyk    schedule 22.07.2018    source источник


Ответы (2)


Лучший способ сделать это — преобразовать вызовы службы в использование cats-mtl.

Для представления Try или Option вы можете использовать MonadError, а для регистрации вы можете использовать FunctorTell. Теперь я не знаю, что именно вы делаете внутри своего userService или standService, но я написал код, чтобы продемонстрировать, как может выглядеть результат:

type Log = List[String]

//inside UserService
def findOneByEmail[F[_]](email: String)
  (implicit F: MonadError[F, Error], W: FunctorTell[F, Log]): F[User] = ???

//inside StandService
def list[F[_]]()
  (implicit F: MonadError[F, Error], W: FunctorTell[F, Log]): F[List[Stand]] = ???

def getStandsByUser[F[_]](email: String)
(implicit F: MonadError[F, Error], W: FunctorTell[F, Log]): F[List[Stand]] =
  for {
    user <- userService.findOneByEmail(email)
    stands <- standService.list()
  } yield stands.filter(stand => user.stands.contains(stand.id))


//here we actually run the function
val result =
  getStandsByUser[WriterT[OptionT[Try, ?], Log, ?] // yields WriterT[OptionT[Try, ?], Log, List[Stand]]
    .run  // yields OptionT[Try, (Log, List[Stand])]
    .value // yields Try[Option[(Log, List[Stand])]]

Таким образом, мы можем избежать всех вызовов liftF и легко компоновать наши различные сервисы, даже если они будут использовать разные преобразователи монад во время выполнения.

person Luka Jacobowitz    schedule 04.08.2018

Если вы посмотрите на определение cats.data.Writer, вы увидите, что это псевдоним cats.data.WriterT с фиксированным эффектом Id.

Что вы хотите сделать, так это использовать WriterT напрямую, а вместо Id использовать OptionT[Try, YourType].

Вот небольшой пример кода того, как этого можно добиться:

object Example {

  import cats.data._
  import cats.implicits._

  type MyType[A] = OptionT[Try, A]

  def myFunction: MyType[Int] = OptionT(Try(Option(1)))

  def main(args: Array[String]): Unit = {
    val tmp: WriterT[MyType, List[String], Int] = for {
      _ <- WriterT.tell[MyType, List[String]](List("Before first invocation"))
      i <- WriterT.liftF[MyType, List[String], Int](myFunction)
      _ <- WriterT.tell[MyType, List[String]](List("After second invocation"))
      j <- WriterT.liftF[MyType, List[String], Int](myFunction)
      _ <- WriterT.tell[MyType, List[String]](List(s"Result is ${i + j}"))
    } yield i + j

    val result: Try[Option[(List[String], Int)]] = tmp.run.value
    println(result)
    // Success(Some((List(Before first invocation, After second invocation, Result is 2),2)))
  }

}

Аннотации типов делают это немного уродливым, но в зависимости от вашего варианта использования вы можете избавиться от них. Как видите, myFunction возвращает результат типа OptionT[Try, Int], а WriterT.lift поместит его в объект записи, который также имеет List[String] для ваших журналов.

person Denis Rosca    schedule 31.07.2018