จะให้เหตุผลเกี่ยวกับความปลอดภัยของกองซ้อนใน 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
}

ไม่ไม่แน่นอน แม้ว่าถ้าเป็นกรณีของการเรียกซ้ำหางโปรดบอกเช่นนั้น แต่ดูเหมือนว่ามันจะไม่ เท่าที่ฉันรู้ว่าแมวทำเวทมนต์บางอย่างเรียกว่าแทรมโปลินเพื่อความปลอดภัย น่าเสียดายที่ฉันไม่สามารถบอกได้เมื่อฟังก์ชั่น trampolined และเมื่อไม่
เลฟเดนิซอฟ

คุณสามารถเขียนซ้ำgoเพื่อใช้เช่นMonad[F]typeclass - มีtailRecMวิธีการที่อนุญาตให้คุณใช้แทรมโพลีนอย่างชัดเจนเพื่อรับประกันว่าฟังก์ชั่นนั้นจะปลอดภัย ฉันอาจจะผิด แต่ถ้าไม่มีคุณพึ่งพาFความปลอดภัยด้วยตัวเอง (เช่นถ้าใช้แทรมโพลีนภายใน) แต่คุณไม่มีทางรู้เลยว่าใครจะเป็นผู้กำหนดคุณFดังนั้นคุณไม่ควรทำเช่นนี้ หากคุณไม่มีการรับประกันว่าFปลอดภัยสแต็กให้ใช้คลาสประเภทที่ให้ไว้tailRecMเพราะมันปลอดภัยสแต็คตามกฎหมาย
Mateusz Kubuszok

1
มันง่ายที่จะให้คอมไพเลอร์พิสูจน์ด้วย@tailrecคำอธิบายประกอบสำหรับฟังก์ชั่น tail rec สำหรับกรณีอื่น ๆ ไม่มีการรับประกันอย่างเป็นทางการใน Scala AFAIK แม้ว่าฟังก์ชั่นของตัวเองจะปลอดภัย แต่ฟังก์ชั่นอื่น ๆ ที่ใช้อยู่อาจจะไม่: /
yǝsʞǝla

คำตอบ:


17

คำตอบก่อนหน้าของฉันที่นี่ให้ข้อมูลพื้นฐานที่อาจมีประโยชน์ แนวคิดพื้นฐานคือเอฟเฟกต์บางประเภทมีflatMapการนำไปใช้งานที่สนับสนุนการเรียกซ้ำแบบปลอดภัยสแต็กโดยตรง - คุณสามารถซ้อนการflatMapโทรอย่างชัดเจนหรือผ่านการเรียกซ้ำอย่างลึกที่สุดเท่าที่คุณต้องการและคุณจะไม่ล้นกองซ้อน

สำหรับเอฟเฟกต์บางประเภทไม่flatMapสามารถซ้อนทับได้อย่างปลอดภัยเนื่องจากความหมายของเอฟเฟกต์ ในกรณีอื่น ๆ อาจเป็นไปได้ที่จะเขียน stack-safe flatMapแต่ implementers อาจตัดสินใจที่จะไม่ทำเพราะประสิทธิภาพหรือข้อควรพิจารณาอื่น ๆ

น่าเสียดายที่ไม่มีวิธีมาตรฐาน (หรือแม้แต่แบบทั่วไป) ที่จะรู้ว่าflatMapประเภทที่ระบุนั้นปลอดภัยสำหรับสแต็กหรือไม่ แมวมีการtailRecMดำเนินการที่ควรจัดให้มีการเรียกใช้แบบ monadic แบบปลอดภัยสำหรับการเรียกใช้ผลแบบ monadic ใด ๆ ที่ถูกกฎหมายและบางครั้งการดูการ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เป็นเพียงการ recursing ผ่านflatMapและเรารู้ว่าPull's Monadตัวอย่างถูกต้องตามกฎหมายซึ่งเป็นหลักฐานที่ดีงามที่Pull' s flatMapมีกองปลอดภัย หนึ่งปัจจัยแทรกซ้อนที่นี่คือตัวอย่างสำหรับการPullมีApplicativeErrorข้อ จำกัด ในการFที่Pull's flatMapไม่ได้ แต่ในกรณีนี้ที่ไม่ได้เปลี่ยนแปลงอะไร

ดังนั้นการtkติดตั้งที่นี่จึงปลอดภัยสำหรับสแต็กเพราะflatMapเปิดPullเป็นสแต็กที่ปลอดภัยและเรารู้ว่าจากการดูการtailRecMใช้งาน (ถ้าเราขุดลึกลงไปอีกหน่อยเราก็จะสามารถเข้าใจได้ว่าflatMapสแต็ก - ปลอดภัยเพราะPullเป็นตัวห่อหุ้มFreeCซึ่งเป็นtrampolined )

อาจไม่ยากอย่างมากที่จะเขียนซ้ำ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เลเยอร์มากขึ้นดังนั้นจึงควรจะดี

เพื่อตอบคำถามของคุณโดยทั่ว ๆ ไปหัวข้อทั้งหมดนี้เป็นความสับสนในสกาล่า คุณไม่ควรที่จะเจาะลึกเกี่ยวกับการใช้งานเหมือนที่เราทำข้างต้นเพื่อทราบว่าประเภทสนับสนุนการเรียกซ้ำแบบ monadic ที่ปลอดภัยหรือไม่ อนุสัญญาที่ดีกว่าเกี่ยวกับเอกสารจะช่วยได้ที่นี่ แต่น่าเสียดายที่เราไม่ได้ทำงานที่ดีมาก คุณสามารถใช้tailRecMเป็น "ปลอดภัย" ได้ตลอดเวลา (ซึ่งเป็นสิ่งที่คุณต้องการทำเมื่อF[_]เป็นแบบทั่วไปอยู่แล้ว) แต่ถึงอย่างนั้นคุณก็เชื่อว่าการMonadใช้งานนั้นถูกต้องตามกฎหมาย

ในการสรุป: มันเป็นสถานการณ์ที่ไม่ดีรอบตัวและในสถานการณ์ที่ละเอียดอ่อนคุณควรเขียนการทดสอบของคุณเองเพื่อยืนยันว่าการใช้งานเช่นนี้ปลอดภัย


ขอบคุณสำหรับคำอธิบาย เกี่ยวกับคำถามเมื่อเราโทรgoจากวิธีอื่นสิ่งใดที่ทำให้เป็นกองซ้อนที่ไม่ปลอดภัย หากเราทำการคำนวณแบบไม่เรียกซ้ำก่อนที่เราจะเรียก Pull.output(hd) >> go(tl, n - m)มันว่าดี?
เลฟเดนิซอฟ

ใช่ว่าน่าจะใช้ได้ (สมมติว่าการคำนวณเองไม่ได้ทำให้กองซ้อนล้น)
เทรวิสบราวน์

ยกตัวอย่างเช่นชนิดของเอฟเฟกต์ใดจะไม่ปลอดภัยสำหรับการเรียกคืนแบบ monadic? ประเภทความต่อเนื่อง?
บ๊อบ

@Bob ถูกต้องถึงแม้ว่า Cats ContT's flatMap จะปลอดภัยแบบกองซ้อน (ผ่านDeferข้อ จำกัด เกี่ยวกับประเภทพื้นฐาน) ฉันคิดอะไรบางอย่างListมากกว่าที่การเรียกซ้ำflatMapไม่ปลอดภัย (แต่มีกฎหมายtailRecM)
เทรวิสบราวน์
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.