ควรสร้างวัตถุ stateful ควรเป็นแบบจำลองชนิดของเอฟเฟกต์หรือไม่?


9

เมื่อใช้สภาพแวดล้อมที่ใช้งานได้เช่น Scala และcats-effectควรสร้างแบบจำลองวัตถุที่มีสภาวะเป็น state หรือไม่?

// not a value/case class
class Service(s: name)

def withoutEffect(name: String): Service =
  new Service(name)

def withEffect[F: Sync](name: String): F[Service] =
  F.delay {
    new Service(name)
  }

การก่อสร้างไม่ได้ทำผิดได้ดังนั้นเราจึงสามารถใช้ typeclass Applyปรับตัวลดลงเช่น

// never throws
def withWeakEffect[F: Applicative](name: String): F[Service] =
  new Service(name).pure[F]

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


2
ใช่การสร้างสถานะที่ไม่แน่นอนเป็นผลข้างเคียง เช่นนี้มันควรจะเกิดขึ้นภายในdelayและกลับF [บริการ] เป็นตัวอย่างดูstartวิธีการในIOมันส่งคืนIO [ไฟเบอร์ [IO,?]]แทนเส้นใยธรรมดา
Luis Miguel

1
สำหรับคำตอบที่สมบูรณ์ในการแก้ไขปัญหานี้โปรดดูที่นี้และนี้
Luis Miguel

คำตอบ:


3

ควรสร้างวัตถุ stateful ควรเป็นแบบจำลองชนิดของเอฟเฟกต์หรือไม่?

หากคุณใช้ระบบเอฟเฟกต์อยู่แล้วน่าจะเป็นRefประเภทที่จะสรุปสถานะที่ไม่แน่นอนได้อย่างปลอดภัย

ดังนั้นผมจึงบอกว่า: รูปแบบวัตถุ stateful Refกับ ตั้งแต่การสร้าง (รวมถึงการเข้าถึง) สิ่งเหล่านี้มีผลอยู่แล้วสิ่งนี้จะทำให้การสร้างบริการมีผลโดยอัตโนมัติเช่นกัน

คำถามข้างต้นนี้ตรงไปตรงมาอย่างเรียบร้อย

หากคุณต้องการจัดการสถานะที่ไม่แน่นอนภายในด้วยตัวเองด้วยตนเองvarคุณต้องตรวจสอบด้วยตัวเองว่าการดำเนินการทั้งหมดที่สัมผัสสถานะนี้ถือเป็นเอฟเฟกต์ (และส่วนใหญ่จะทำให้เธรดปลอดภัย) ซึ่งน่าเบื่อและผิดพลาดได้ง่าย สิ่งนี้สามารถทำได้และฉันเห็นด้วยกับคำตอบของ @ atl ว่าคุณไม่จำเป็นต้องสร้างวัตถุ stateful อย่างมีประสิทธิภาพ (ตราบใดที่คุณสามารถมีชีวิตอยู่กับการสูญเสียความสมบูรณ์ของ Referential) แต่ทำไมไม่ช่วยตัวเองให้ลำบาก เครื่องมือของระบบเอฟเฟกต์ของคุณไปตลอดทาง?


ฉันเดาว่าทั้งหมดนี้บริสุทธิ์และไม่แน่นอน เพียง แต่ไม่อ้างอิงอย่างโปร่งใสเนื่องจากอินสแตนซ์ที่ได้นั้นแตกต่างกันในแต่ละครั้ง เป็นเวลาที่ดีในการใช้เอฟเฟกต์หรือไม่?

หากคำถามของคุณสามารถ rephrased เป็น

เป็นประโยชน์เพิ่มเติม (ด้านบนของการดำเนินงานที่ถูกต้องโดยใช้ "ประเภทที่อ่อนแอกว่า") ของความโปร่งใสในการอ้างอิงและเหตุผลในท้องถิ่นเพียงพอที่จะแสดงให้เห็นถึงการใช้เอฟเฟ็กต์ชนิด (ซึ่งจะต้องมีการใช้ การสร้าง?

จากนั้นได้แน่นอน

เพื่อให้ตัวอย่างว่าทำไมสิ่งนี้ถึงมีประโยชน์:

การทำงานต่อไปนี้ใช้ได้ผลถึงแม้ว่าการสร้างบริการจะไม่ส่งผล:

val service = makeService(name)
for {
  _ <- service.doX()
  _ <- service.doY()
} yield Ack.Done

แต่ถ้าคุณปรับเปลี่ยนสิ่งนี้ตามด้านล่างคุณจะไม่ได้รับข้อผิดพลาดในการคอมไพล์ แต่คุณจะเปลี่ยนพฤติกรรมและน่าจะเป็นข้อผิดพลาด หากคุณประกาศว่ามีผลmakeServiceบังคับใช้การเปลี่ยนโครงสร้างจะไม่ตรวจสอบและถูกคอมไพเลอร์ปฏิเสธ

for {
  _ <- makeService(name).doX()
  _ <- makeService(name).doY()
} yield Ack.Done

ได้รับการตั้งชื่อของวิธีการเป็นmakeService(และด้วยพารามิเตอร์เช่นกัน) ควรทำให้ชัดเจนว่าวิธีการทำอะไรและว่า refactoring ไม่ใช่สิ่งที่ปลอดภัยที่จะทำ แต่ "เหตุผลท้องถิ่น" หมายความว่าคุณไม่ต้องมอง ที่การตั้งชื่อแบบแผนและการดำเนินการของmakeServiceการคิดออก: การแสดงออกใด ๆ ที่ไม่สามารถสับด้วยเครื่องจักร (deduplicated ทำให้ขี้เกียจทำกระตือรือร้นรหัสตายกำจัด, ขนาน, ล่าช้า, แคช, ล้างออกจากแคช ฯลฯ ) โดยไม่เปลี่ยนพฤติกรรม ( เช่นไม่ได้เป็น "บริสุทธิ์") ควรจะพิมพ์ที่มีประสิทธิภาพ


2

บริการ stateful อ้างถึงอะไรในกรณีนี้

คุณหมายถึงว่ามันจะใช้ผลข้างเคียงเมื่อวัตถุถูกสร้างขึ้น? สำหรับสิ่งนี้ความคิดที่ดีกว่าคือการมีวิธีการใช้ผลข้างเคียงเมื่อแอปพลิเคชันของคุณเริ่มทำงาน แทนที่จะวิ่งระหว่างก่อสร้าง

หรือคุณกำลังบอกว่ามันมีสถานะที่ไม่แน่นอนในการให้บริการ? ตราบใดที่ไม่สามารถเปิดเผยสถานะภายในได้ก็ควรเป็นไร คุณเพียงแค่ต้องให้วิธีบริสุทธิ์ (อ้างอิงโปร่งใส) เพื่อสื่อสารกับบริการ

หากต้องการขยายในจุดที่สองของฉัน:

สมมติว่าเรากำลังสร้างฐานข้อมูลหน่วยความจำ

class InMemoryDB(private val hashMap: ConcurrentHashMap[String, String]) {
  def getId(s: String): IO[String] = ???
  def setId(s: String): IO[Unit] = ???
}

object InMemoryDB {
  def apply(hashMap: ConcurrentHashMap[String, String]) = new InMemoryDB(hashMap)
}

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

หากคุณใช้Refจาก cat-effect สิ่งที่ฉันจะทำตามปกติคือไปยังflatMapจุดอ้างอิงที่จุดเริ่มต้นดังนั้นชั้นเรียนของคุณไม่จำเป็นต้องมีประสิทธิภาพ

object Effectful extends IOApp {

  class InMemoryDB(storage: Ref[IO, Map[String, String]]) {
    def getId(s: String): IO[String] = ???
    def setId(s: String): IO[Unit] = ???
  }

  override def run(args: List[String]): IO[ExitCode] = {
    for {
      storage <- Ref.of[IO, Map[String, String]](Map.empty[String, String])
      _ = app(storage)
    } yield ExitCode.Success
  }

  def app(storage: Ref[IO, Map[String, String]]): InMemoryDB = {
    new InMemoryDB(storage)
  }
}

OTOH ถ้าคุณกำลังเขียนบริการสาธารณะหรือห้องสมุดที่ขึ้นอยู่กับวัตถุที่เป็นรัฐ (สมมติว่ามีการทำงานพร้อมกันหลายตัว) และคุณไม่ต้องการให้ผู้ใช้สนใจสิ่งที่จะเริ่มต้น

จากนั้นใช่จะต้องถูกห่อด้วยเอฟเฟกต์ คุณสามารถใช้สิ่งที่ต้องการResource[F, MyStatefulService]เพื่อให้แน่ใจว่าทุกอย่างถูกปิดอย่างถูกต้อง หรือF[MyStatefulService]ถ้าไม่มีอะไรจะปิด


"คุณเพียงแค่ต้องให้วิธีการวิธีบริสุทธิ์ในการสื่อสารกับบริการ" หรืออาจจะตรงกันข้าม: การสร้างเริ่มต้นของสถานะภายในหมดจดไม่จำเป็นต้องมีผลกระทบ แต่การดำเนินการใด ๆ กับบริการที่มีปฏิสัมพันธ์กับรัฐที่ไม่แน่นอนใน วิธีใดก็ตามจะต้องมีการทำเครื่องหมายที่มีประสิทธิภาพ (เพื่อหลีกเลี่ยงการเกิดอุบัติเหตุเช่นval neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5))
Thilo

หรือมาจากอีกด้านหนึ่ง: หากคุณทำให้การสร้างบริการนั้นมีผลหรือไม่ไม่สำคัญจริงๆ แต่ไม่ว่าคุณจะไปทางไหนการโต้ตอบกับบริการนั้นไม่ว่าจะด้วยวิธีใดก็ตามจะต้องมีประสิทธิภาพ
Thilo

1
@thilo ใช่คุณพูดถูก สิ่งที่ฉันหมายถึงpureคือมันต้องมีความโปร่งใสในการอ้างอิง เช่นพิจารณาตัวอย่างกับอนาคต val x = Future {... }และdef x = Future { ... }หมายถึงสิ่งที่แตกต่าง (สิ่งนี้สามารถกัดคุณเมื่อคุณ refactoring รหัสของคุณ) แต่ไม่เป็นกรณีที่มีผลกระทบแมว, monix หรือ zio
atl
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.