Làm thế nào để lý do về an toàn ngăn xếp trong Scala Mèo / fs2?


13

Đây là một đoạn mã từ tài liệu cho fs2 . Hàm gođược đệ quy. Câu hỏi là làm thế nào để chúng ta biết nếu nó là stack an toàn và làm thế nào để suy luận nếu bất kỳ chức năng nào là stack an toàn?

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)

Nó cũng sẽ được ngăn xếp an toàn nếu chúng ta gọi gotừ một phương thức khác?

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
}

Không, không chính xác. Mặc dù nếu đây là trường hợp đệ quy đuôi xin vui lòng nói như vậy, nhưng có vẻ như không phải vậy. Theo như tôi biết thì mèo thực hiện một số phép thuật gọi là trampolining để đảm bảo an toàn cho ngăn xếp. Thật không may, tôi không thể biết khi nào một chức năng bị trampolined và khi nào thì không.
Lev Denisov

Bạn có thể viết lại gođể sử dụng ví dụ như Monad[F]typeclass - có tailRecMphương pháp cho phép bạn thực hiện trampoline một cách rõ ràng để đảm bảo rằng chức năng sẽ được xếp chồng an toàn. Tôi có thể sai nhưng không có nó, bạn đang dựa vào Fviệc tự mình xếp chồng an toàn (ví dụ: nếu nó thực hiện trampoline trong nội bộ), nhưng bạn không bao giờ biết ai sẽ xác định bạn F, vì vậy bạn không nên làm điều này. Nếu bạn không có gì đảm bảo rằng Fstack an toàn, hãy sử dụng một loại lớp cung cấp tailRecMbởi vì nó là stack an toàn theo luật.
Mateusz Kubuszok

1
Thật dễ dàng để trình biên dịch chứng minh nó với @tailrecchú thích cho các hàm rec đuôi. Đối với các trường hợp khác, không có đảm bảo chính thức trong Scala AFAIK. Ngay cả khi chức năng đó an toàn, các chức năng khác mà nó gọi có thể không phải là: /.
yǝsʞǝla

Câu trả lời:


17

Câu trả lời trước của tôi ở đây cung cấp một số thông tin cơ bản có thể hữu ích. Ý tưởng cơ bản là một số loại hiệu ứng có các flatMaptriển khai hỗ trợ đệ quy an toàn ngăn xếp trực tiếp, bạn có thể lồng flatMapcác cuộc gọi một cách rõ ràng hoặc thông qua đệ quy sâu như bạn muốn và bạn sẽ không tràn vào ngăn xếp.

Đối với một số loại hiệu ứng, không thể flatMapan toàn cho ngăn xếp, vì ngữ nghĩa của hiệu ứng. Trong các trường hợp khác, có thể viết một ngăn xếp an toàn flatMap, nhưng những người triển khai có thể đã quyết định không vì hiệu suất hoặc các cân nhắc khác.

Thật không may, không có cách tiêu chuẩn (hoặc thậm chí thông thường) để biết liệu loại flatMapcho trước có an toàn với ngăn xếp hay không. Mèo bao gồm một tailRecMhoạt động sẽ cung cấp đệ quy đơn nguyên an toàn cho bất kỳ loại hiệu ứng đơn nguyên hợp pháp nào, và đôi khi nhìn vào một tailRecMtriển khai được biết là hợp pháp có thể cung cấp một số gợi ý về việc liệu có flatMapan toàn cho ngăn xếp hay không. Trong trường hợp của Pullnó trông như thế này :

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

Đây tailRecMchỉ là recursing qua flatMap, và chúng ta biết rằng Pull's Monaddụ là hợp pháp , đó là bằng chứng khá tốt mà Pull' s flatMaplà chồng-an toàn. Một yếu tố phức tạp ở đây là ví dụ cho Pullcó một ApplicativeErrorràng buộc đối với Fđiều đó PullflatMapkhông, nhưng trong trường hợp này không thay đổi gì cả.

Vì vậy, việc tktriển khai ở đây là stack-safe vì flatMapon Pulllà stack-safe và chúng tôi biết rằng từ việc xem xét việc tailRecMthực hiện của nó . (Nếu chúng ta đào sâu hơn một chút, chúng ta có thể nhận ra rằng đó flatMaplà an toàn ngăn xếp bởi vì Pullvề cơ bản là một trình bao bọc cho FreeC, được trampolined .)

Có lẽ sẽ rất khó để viết lại tkvề mặt tailRecM, mặc dù chúng ta phải thêm các ApplicativeErrorràng buộc không cần thiết . Tôi đoán các tác giả của tài liệu đã chọn không làm điều đó cho rõ ràng, và vì họ biết rằng PullflatMapvẫn ổn.


Cập nhật: đây là một tailRecMbản dịch khá cơ học :

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

Lưu ý rằng không có đệ quy rõ ràng.


Câu trả lời cho câu hỏi thứ hai của bạn phụ thuộc vào phương thức khác trông như thế nào, nhưng trong trường hợp ví dụ cụ thể của bạn, >>sẽ chỉ dẫn đến nhiều flatMaplớp hơn , vì vậy nó sẽ ổn.

Để giải quyết câu hỏi của bạn nói chung hơn, toàn bộ chủ đề này là một mớ hỗn độn khó hiểu trong Scala. Bạn không cần phải đào sâu vào các triển khai như chúng tôi đã làm ở trên chỉ để biết liệu một loại có hỗ trợ đệ quy đơn nguyên an toàn ngăn xếp hay không. Các quy ước tốt hơn xung quanh tài liệu sẽ là một trợ giúp ở đây, nhưng thật không may, chúng tôi không làm tốt công việc đó. Bạn luôn có thể sử dụng tailRecMđể "an toàn" (đó là điều bạn sẽ muốn làm khi F[_]chung chung), nhưng ngay cả khi đó bạn vẫn tin tưởng rằng việc Monadtriển khai là hợp pháp.

Tóm lại: đó là một tình huống tồi tệ xung quanh và trong các tình huống nhạy cảm, bạn chắc chắn nên viết các bài kiểm tra của riêng mình để xác minh rằng các triển khai như thế này là an toàn cho ngăn xếp.


Cám ơn vì đã giải thích. Liên quan đến câu hỏi khi chúng ta gọi gotừ một phương thức khác, điều gì có thể làm cho nó xếp chồng không an toàn? Nếu chúng tôi thực hiện một số tính toán không đệ quy trước khi chúng tôi gọi Pull.output(hd) >> go(tl, n - m)thì có ổn không?
Lev Denisov

Vâng, điều đó sẽ ổn thôi (tất nhiên là giả sử tính toán không tràn vào ngăn xếp).
Travis Brown

Loại hiệu ứng nào, chẳng hạn, sẽ không an toàn cho phép đệ quy đơn nguyên? Các loại tiếp tục?
bob

@ Bob Đúng vậy, mặc dù Mèo của ContT's flatMap thực sự chồng an toàn (thông qua một Deferchế vào loại cơ bản). Tôi đã suy nghĩ nhiều hơn về một cái gì đó như List, trong đó đệ quy thông qua flatMapkhông phải là stack-safe ( tailRecMmặc dù nó có hợp pháp ).
Travis Brown
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.