Як міркувати про безпеку стеків у Scala Cats / fs2?


13

Ось фрагмент коду з документації на fs2 . Функція goрекурсивна. Питання полягає в тому, як ми можемо знати, чи безпечний стек і як обґрунтувати, чи якась функція є безпечною?

import fs2._
// import fs2._

def tk[F[_],O](n: Long): Pipe[F,O,O] = {
  def go(s: Stream[F,O], n: Long): Pull[F,O,Unit] = {
    s.pull.uncons.flatMap {
      case Some((hd,tl)) =>
        hd.size match {
          case m if m <= n => Pull.output(hd) >> go(tl, n - m)
          case m => Pull.output(hd.take(n.toInt)) >> Pull.done
        }
      case None => Pull.done
    }
  }
  in => go(in,n).stream
}
// tk: [F[_], O](n: Long)fs2.Pipe[F,O,O]

Stream(1,2,3,4).through(tk(2)).toList
// res33: List[Int] = List(1, 2)

Чи буде це також безпечним стеком, якщо ми зателефонуємо goз іншого методу?

def tk[F[_],O](n: Long): Pipe[F,O,O] = {
  def go(s: Stream[F,O], n: Long): Pull[F,O,Unit] = {
    s.pull.uncons.flatMap {
      case Some((hd,tl)) =>
        hd.size match {
          case m if m <= n => otherMethod(...)
          case m => Pull.output(hd.take(n.toInt)) >> Pull.done
        }
      case None => Pull.done
    }
  }

  def otherMethod(...) = {
    Pull.output(hd) >> go(tl, n - m)
  }

  in => go(in,n).stream
}

Ні, не зовсім. Хоча якщо це стосується хвостової рекурсії, будь ласка, скажіть так, але здається, що це не так. Наскільки я знаю, коти роблять якусь магію під назвою батут, щоб забезпечити безпеку штабелів. На жаль, я не можу сказати, коли функція утрамбована, а коли ні.
Лев Денисов

Ви можете переписати goдля використання, наприклад, Monad[F]typeclass - існує tailRecMметод, який дозволяє вам виконувати батут явно, щоб гарантувати, що ця функція буде безпечною. Я можу помилятися, але без цього ви покладаєтесь на Fте, щоб захищати їх самостійно (наприклад, якщо він реалізує батут внутрішньо), але ви ніколи не знаєте, хто визначить ваш F, тому не варто цього робити. Якщо у вас немає гарантій щодо Fбезпечності стека, використовуйте клас типу, який надається, tailRecMоскільки він є безпечним для стеків за законом.
Матеуш Кубушок

1
Легко дозволити компілятору довести це за допомогою @tailrecпримітки для функцій хвостового запису. В інших випадках у Scala AFAIK офіційних гарантій немає. Навіть якщо сама функція безпечна, іншими функціями, які вона викликає, можуть бути: /.
yǝsʞǝla

Відповіді:


17

Моя попередня відповідь тут дає деяку довідкову інформацію, яка може бути корисною. Основна ідея полягає в тому, що деякі типи ефектів мають flatMapреалізації, які підтримують безпечну рекурсію безпосередньо - ви можете вводити flatMapвиклики або явно, або через рекурсію так глибоко, як вам захочеться, і ви не переповнюєте стек.

Для деяких типів ефектів неможливо flatMapбути безпечними для зберігання через семантику ефекту. В інших випадках можливо створити безпечний стек flatMap, але реалізатори, можливо, вирішили цього не зробити через ефективність чи інші міркування.

На жаль, не існує стандартного (або навіть звичайного) способу дізнатися, чи flatMapбезпечний для даного типу безпечний стек. Коти включають в себе tailRecMоперацію, яка повинна забезпечувати безпечну монадичну рекурсію для будь-якого законного типу монадичного ефекту, а іноді, дивлячись на tailRecMреалізацію, яка, як відомо, є законною, може дати деякі підказки щодо того, чи flatMapє безпечним стек. У разі Pullце виглядає як це :

def tailRecM[A, B](a: A)(f: A => Pull[F, O, Either[A, B]]) =
  f(a).flatMap {
    case Left(a)  => tailRecM(a)(f)
    case Right(b) => Pull.pure(b)
  }

Це tailRecMпросто повторюється flatMap, і ми знаємо, що екземпляр Pull' є законним , що є досить хорошим доказом того , що це безпечно для стеків. Один ускладнює фактор тут є те , що екземпляр для має обмеження на те , що «s не робить, але в даному випадку це не змінює нічого.MonadPullflatMapPullApplicativeErrorFPullflatMap

Таким чином, tkреалізація тут стека безпечна , тому що flatMapна Pullце стек безпечно, і ми знаємо , що, дивлячись на його tailRecMреалізацію. (Якби ми копали трохи глибше, ми могли б зрозуміти, що flatMapце безпечно для штабелів, оскільки Pullце, по суті, обгортка, для FreeCякої затоптана .)

Напевно, було б не важко переписати tkз точки зору tailRecM, хоча нам доведеться додати інакше зайве ApplicativeErrorобмеження. Я здогадуюсь, автори документації вирішили не робити цього для ясності, і тому, що вони знали Pull, що flatMapце добре.


Оновлення: ось досить механічний tailRecMпереклад:

import cats.ApplicativeError
import fs2._

def tk[F[_], O](n: Long)(implicit F: ApplicativeError[F, Throwable]): Pipe[F, O, O] =
  in => Pull.syncInstance[F, O].tailRecM((in, n)) {
    case (s, n) => s.pull.uncons.flatMap {
      case Some((hd, tl)) =>
        hd.size match {
          case m if m <= n => Pull.output(hd).as(Left((tl, n - m)))
          case m => Pull.output(hd.take(n.toInt)).as(Right(()))
        }
      case None => Pull.pure(Right(()))
    }
  }.stream

Зауважте, що явної рекурсії немає.


Відповідь на ваше друге запитання залежить від того, як виглядає інший метод, але у випадку вашого конкретного прикладу >>просто вийде більше flatMapшарів, тому воно повинно бути добре.

Щоб вирішити своє питання в більш загальному плані, вся ця тема є заплутаним безладом у Scala. Вам не потрібно було б копатись у реалізаціях, як ми це робили вище, просто щоб знати, чи підтримує тип монадичної рекурсії безпечний для стеків чи ні. Тут можна допомогти кращим конвенціям навколо документації, але, на жаль, ми не дуже добре працюємо в цьому. Ви завжди можете використовувати, tailRecMщоб бути "безпечним" (це все, що ви хочете робити, коли все-таки F[_]є загальним), але навіть тоді ви довіряєте, що Monadреалізація є законною.

Підсумовуючи це: це погана ситуація навколо, і у чутливих ситуаціях ви обов'язково повинні написати власні тести, щоб переконатися, що такі реалізації є безпечними для стеків.


Дякую за пояснення. Що стосується питання, коли ми телефонуємо goз іншого методу, що може зробити його стек небезпечним? Якщо ми зробимо кілька нерекурсивних розрахунків, перш ніж зателефонувати, Pull.output(hd) >> go(tl, n - m)чи це добре?
Лев Денисов

Так, це повинно бути добре (якщо, звичайно, сам розрахунок не переповнює стек).
Тревіс Браун

Який тип ефекту, наприклад, не був би безпечним для стека для монадичної рекурсії? Тип продовження?
боб

@bob право, хоча кішки - х ContT«s flatMap є на насправді стек безпечно (через Deferобмеження на базовий тип). Я думав більше про щось на кшталт List, де повторне повторення flatMapне є безпечним для стеків (все-таки є законним tailRecM, хоча).
Тревіс Браун
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.