Кошки: метод tailRecM без хвостовой рекурсии для монад

В cats, когда монада создается с использованием признака Monad, должна быть предоставлена ​​реализация метода tailRecM.

У меня есть сценарий ниже, который я обнаружил невозможным для хвостовой рекурсивной реализации tailRecM

  sealed trait Tree[+A]

  final case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]

  final case class Leaf[A](value: A) extends Tree[A]

  implicit val treeMonad = new Monad[Tree] {

    override def pure[A](value: A): Tree[A] = Leaf(value)

    override def flatMap[A, B](initial: Tree[A])(func: A => Tree[B]): Tree[B] =
      initial match {
        case Branch(l, r) => Branch(flatMap(l)(func), flatMap(r)(func))
        case Leaf(value) => func(value)
      }

    //@tailrec
    override def tailRecM[A, B](a: A)(func: (A) => Tree[Either[A, B]]): Tree[B] = {
      func(a) match {
        case Branch(l, r) =>
          Branch(
            flatMap(l) {
              case Right(l) => pure(l)
              case Left(l) => tailRecM(l)(func)
            },
            flatMap(r){
              case Right(r) => pure(r)
              case Left(r) => tailRecM(r)(func)
            }
          )

        case Leaf(Left(value)) => tailRecM(value)(func)

        case Leaf(Right(value)) => Leaf(value)
      }
    }
  }

1) Согласно приведенному выше примеру, как этот метод tailRecM можно использовать для оптимизации вызова метода flatMap? Реализация метода flatMap переопределяется/модифицируется tailRecM во время компиляции?

2) Если tailRecM не является хвостовой рекурсией, как указано выше, будет ли он по-прежнему эффективен, чем использование исходного метода flatMap?

Пожалуйста, поделитесь своими мыслями.


person tharindu_DG    schedule 12.06.2017    source источник


Ответы (2)


Связь между tailRecM и flatMap

Чтобы ответить на ваш первый вопрос, следующий код является частью FlatMapLaws.scala из cats-laws. Он проверяет согласованность между методами flatMap и tailRecM.

/**
 * It is possible to implement flatMap from tailRecM and map
 * and it should agree with the flatMap implementation.
 */
def flatMapFromTailRecMConsistency[A, B](fa: F[A], fn: A => F[B]): IsEq[F[B]] = {
  val tailRecMFlatMap = F.tailRecM[Option[A], B](Option.empty[A]) {
    case None => F.map(fa) { a => Left(Some(a)) }
    case Some(a) => F.map(fn(a)) { b => Right(b) }
  }

  F.flatMap(fa)(fn) <-> tailRecMFlatMap
}

Это показывает, как реализовать flatMap из tailRecM, и неявно предполагает, что компилятор не будет делать это автоматически. Пользователь Monad должен решить, когда имеет смысл использовать tailRecM вместо flatMap.

В этом блоге есть хорошие примеры Scala, чтобы объяснить, когда tailRecM пригодится. Он следует за статьей о PureScript Фила Фримена, в которой изначально был представлен этот метод.

Это объясняет недостатки использования flatMap для монадной композиции:

Эта характеристика Scala ограничивает полезность монадической композиции, когда flatMap может вызывать монадическую функцию f, которая затем может вызывать flatMap и т. д.

В отличие от реализации на основе tailRecM:

Это гарантирует большую безопасность для пользователя класса типов FlatMap, но это будет означать, что каждый разработчик экземпляров должен будет предоставить безопасный tailRecM.

Многие из предложенных методов у кошек используют монадную композицию. Таким образом, даже если вы не используете его напрямую, реализация tailRecM позволяет более эффективно компоновать другие монады.

Реализация для дерева

В другом ответе @nazarii-bardiuk предоставляет реализацию tailRecM, которая является хвостовой рекурсией, но не проходит упомянутый выше тест согласованности flatMap/tailRecM. Древовидная структура не перестраивается должным образом после рекурсии. Исправленная версия ниже:

def tailRecM[A, B](arg: A)(func: A => Tree[Either[A, B]]): Tree[B] = {
  @tailrec
  def loop(toVisit: List[Tree[Either[A, B]]], 
           toCollect: List[Option[Tree[B]]]): List[Tree[B]] =
    toVisit match {
      case Branch(l, r) :: next =>
        loop(l :: r :: next, None :: toCollect)

      case Leaf(Left(value)) :: next =>
        loop(func(value) :: next, toCollect)

      case Leaf(Right(value)) :: next =>
        loop(next, Some(pure(value)) :: toCollect)

      case Nil =>
        toCollect.foldLeft(Nil: List[Tree[B]]) { (acc, maybeTree) =>
          maybeTree.map(_ :: acc).getOrElse {
            val left :: right :: tail = acc
            branch(left, right) :: tail
          }
        }
    }

  loop(List(func(arg)), Nil).head
}

(суть с тестом)

Вы, наверное, в курсе, но ваш пример (а также ответ @nazarii-bardiuk) используется в книге Scala with Cats от Ноэля Уэлша и Дэйва Гернелла (настоятельно рекомендуется).

person hjmeijer    schedule 05.07.2018

Иногда есть способ заменить стек вызовов явным списком.

Здесь toVisit отслеживает ветки, ожидающие обработки.

И toCollect хранит ветки, ожидающие объединения, до тех пор, пока соответствующая ветвь не будет обработана.

override def tailRecM[A, B](a: A)(f: (A) => Tree[Either[A, B]]): Tree[B] = {
  @tailrec
  def go(toVisit: List[Tree[Either[A, B]]],
         toCollect: List[Tree[B]]): List[Tree[B]] = toVisit match {
    case (tree :: tail) =>
      tree match {
        case Branch(l, r) =>
          l match {
            case Branch(_, _) => go(l :: r :: tail, toCollect)
            case Leaf(Left(a)) => go(f(a) :: r :: tail, toCollect)
            case Leaf(Right(b)) => go(r :: tail, pure(b) +: toCollect)
          }
        case Leaf(Left(a)) => go(f(a) :: tail, toCollect)
        case Leaf(Right(b)) =>
          go(tail,
             if (toCollect.isEmpty) pure(b) +: toCollect
             else Branch(toCollect.head, pure(b)) :: toCollect.tail)
      }
    case Nil => toCollect
  }

  go(f(a) :: Nil, Nil).head
}

Из кошачьего билета зачем использовать tailRecM

tailRecM не взорвет стек (как и почти любая программа JVM, она может OOM) для любой из монад в кошках.

а потом

Без безопасности tailRecM (или рекурсивной flatMap) такие библиотеки, как iteratee.io, не могут быть безопасно написаны, поскольку они требуют монадической рекурсии.

и другой билет гласит, что клиенты cats.Monad должны знать, что некоторые монады не имеют stacksafe tailRecM

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

person Nazarii Bardiuk    schedule 13.06.2017
comment
Большое спасибо за ответ. Думаю, вы ответили на мой второй вопрос. Есть идеи по поводу первого? - person tharindu_DG; 13.06.2017
comment
@tharindu_DG добавил несколько ссылок на билеты для кошек - person Nazarii Bardiuk; 13.06.2017
comment
@NazariiBardiuk похоже, что реализация неверна, на простом примере она не работает: Branch(Branch(Leaf(31), Branch(Leaf(11), Leaf(2))), Leaf(12)).tailRecM[Tree, String](_.map(i => Right(i.toString))) shouldBe tree.map(_.toString) - person Ilya Murzinov; 02.05.2018