Có nên xây dựng các đối tượng trạng thái được mô hình hóa với một loại hiệu ứng?


9

Khi sử dụng một môi trường chức năng như Scala và cats-effect, liệu việc xây dựng các đối tượng trạng thái có nên được mô hình hóa với một loại hiệu ứng không?

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

Cấu trúc không dễ đọc, vì vậy chúng ta có thể sử dụng một kiểu chữ yếu hơn như Apply.

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

Tôi đoán tất cả những điều này là tinh khiết và xác định. Chỉ không tham chiếu minh bạch vì trường hợp kết quả là khác nhau mỗi lần. Đó có phải là thời điểm tốt để sử dụng một loại hiệu ứng? Hoặc sẽ có một mô hình chức năng khác nhau ở đây?


2
Có, tạo ra trạng thái đột biến là một tác dụng phụ. Như vậy, nó sẽ xảy ra bên trong a delayvà trả về F [Dịch vụ] . Ví dụ, xem startphương thức trên IO , nó trả về IO [Fiber [IO ,?]] , Thay vì sợi đơn giản .
Luis Miguel Mejía Suárez

1
Để có câu trả lời đầy đủ cho vấn đề này, xin vui lòng xem cái này & cái này .
Luis Miguel Mejía Suárez

Câu trả lời:


3

Có nên xây dựng các đối tượng trạng thái được mô hình hóa với một loại hiệu ứng?

Nếu bạn đã sử dụng một hệ thống hiệu ứng, rất có thể nó có một Refloại để đóng gói một cách an toàn trạng thái có thể thay đổi.

Vì vậy, tôi nói: mô hình đối tượng nhà nước vớiRef . Vì việc tạo (cũng như quyền truy cập) đã là một hiệu ứng, điều này sẽ tự động làm cho việc tạo dịch vụ trở nên hiệu quả.

Điều này gọn gàng bên bước câu hỏi ban đầu của bạn.

Nếu bạn muốn tự quản lý trạng thái có thể thay đổi nội bộ một cách thường xuyên, varbạn phải tự đảm bảo rằng tất cả các thao tác chạm vào trạng thái này đều được coi là hiệu ứng (và rất có thể cũng được thực hiện an toàn theo luồng), rất tẻ nhạt và dễ bị lỗi. Điều này có thể được thực hiện và tôi đồng ý với câu trả lời của @ atl rằng bạn không nhất thiết phải tạo ra đối tượng có trạng thái hiệu quả (miễn là bạn có thể sống với sự mất tính toàn vẹn tham chiếu), nhưng tại sao không tự cứu mình khỏi rắc rối và nắm lấy Các công cụ của hệ thống hiệu ứng của bạn tất cả các cách?


Tôi đoán tất cả những điều này là tinh khiết và xác định. Chỉ không tham chiếu minh bạch vì trường hợp kết quả là khác nhau mỗi lần. Đó có phải là thời điểm tốt để sử dụng một loại hiệu ứng?

Nếu câu hỏi của bạn có thể được đăng lại như

Các lợi ích bổ sung (trên cùng của việc triển khai hoạt động chính xác bằng cách sử dụng "kiểu chữ yếu hơn") về tính minh bạch tham chiếu và lý luận cục bộ đủ để biện minh cho việc sử dụng loại hiệu ứng (đã được sử dụng cho truy cập và đột biến trạng thái) cũng cho trạng thái sự sáng tạo ?

sau đó: Vâng, hoàn toàn .

Để đưa ra một ví dụ về lý do tại sao điều này hữu ích:

Những điều sau đây hoạt động tốt, mặc dù việc tạo dịch vụ không tạo ra hiệu quả:

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

Nhưng nếu bạn cấu trúc lại lỗi này như dưới đây, bạn sẽ không gặp lỗi thời gian biên dịch, nhưng bạn sẽ thay đổi hành vi và rất có thể đã đưa ra lỗi. Nếu bạn đã khai báo hiệu makeServicequả, việc tái cấu trúc sẽ không kiểm tra kiểu và bị trình biên dịch từ chối.

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

Cấp phép đặt tên cho phương thức là makeService(và với một tham số nữa) sẽ làm cho nó khá rõ ràng phương thức đó làm gì và việc tái cấu trúc không phải là một điều an toàn để làm, nhưng "lý luận cục bộ" có nghĩa là bạn không cần phải xem tại các quy ước đặt tên và việc thực hiện makeServiceđể tìm ra: Bất kỳ biểu thức nào không thể xáo trộn một cách máy móc (bị lặp lại, lười biếng, háo hức, loại bỏ mã chết, song song, trì hoãn, lưu trữ, xóa khỏi bộ đệm, v.v.) mà không thay đổi hành vi ( tức là không "thuần") nên được gõ như hiệu quả.


2

Dịch vụ nhà nước đề cập đến gì trong trường hợp này?

Bạn có nghĩa là nó sẽ thực hiện một hiệu ứng phụ khi một đối tượng được xây dựng? Đối với điều này, một ý tưởng tốt hơn sẽ là có một phương thức chạy hiệu ứng phụ khi ứng dụng của bạn bắt đầu. Thay vì chạy nó trong quá trình xây dựng.

Hoặc có thể bạn đang nói rằng nó giữ một trạng thái có thể thay đổi trong dịch vụ? Miễn là trạng thái đột biến bên trong không được phơi bày, nó sẽ ổn. Bạn chỉ cần cung cấp một phương thức thuần túy (tham chiếu minh bạch) để liên lạc với dịch vụ.

Để mở rộng điểm thứ hai của tôi:

Giả sử chúng ta đang xây dựng một db trong bộ nhớ.

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, điều này không cần phải có hiệu lực, vì điều tương tự đang xảy ra nếu bạn thực hiện cuộc gọi mạng. Mặc dù, bạn cần chắc chắn rằng chỉ có một thể hiện của lớp này.

Nếu bạn đang sử dụng Reftừ hiệu ứng mèo, những gì tôi thường làm, là tham flatMapchiếu tại điểm vào, vì vậy lớp của bạn không phải có hiệu lực.

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, nếu bạn đang viết một dịch vụ chia sẻ hoặc một thư viện phụ thuộc vào một đối tượng trạng thái (giả sử nhiều nguyên tắc đồng thời) và bạn không muốn người dùng của mình quan tâm đến việc khởi tạo cái gì.

Sau đó, vâng, nó phải được bọc trong một hiệu ứng. Bạn có thể sử dụng một cái gì đó như, Resource[F, MyStatefulService]để đảm bảo rằng mọi thứ được đóng lại đúng cách. Hoặc chỉ F[MyStatefulService]khi không có gì để đóng.


"Bạn chỉ cần cung cấp cho phương thức một phương thức thuần túy để giao tiếp với dịch vụ" Hoặc có thể ngược lại: Xây dựng ban đầu trạng thái hoàn toàn bên trong không cần phải có hiệu lực, nhưng bất kỳ hoạt động nào trên dịch vụ tương tác với trạng thái có thể thay đổi đó trong bất kỳ cách nào sau đó cần phải được đánh dấu hiệu quả (để tránh tai nạn như val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5))
Thilo

Hoặc đến từ phía bên kia: Nếu bạn thực hiện việc tạo ra dịch vụ đó có hiệu quả hay không thì không thực sự quan trọng. Nhưng cho dù bạn đi theo cách nào, việc tương tác với dịch vụ đó theo bất kỳ cách nào cũng phải có hiệu quả (vì nó mang trạng thái đột biến bên trong sẽ bị ảnh hưởng bởi các tương tác này).
Thilo

1
@thilo Vâng, bạn nói đúng. Điều tôi muốn nói purelà nó phải được minh bạch. ví dụ như xem xét một ví dụ với Tương lai. val x = Future {... }def x = Future { ... }có nghĩa là một điều khác nhau. (Điều này có thể cắn bạn khi bạn tái cấu trúc mã của bạn) Nhưng, đó không phải là trường hợp với hiệu ứng mèo, monix hoặc zio.
atl
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.