Làm thế nào để lập trình chức năng xử lý tình huống trong đó cùng một đối tượng được tham chiếu từ nhiều nơi?


10

Tôi đang đọc và nghe rằng mọi người (cũng trên trang web này) thường xuyên ca ngợi mô hình lập trình chức năng, nhấn mạnh rằng nó tốt như thế nào khi có mọi thứ bất biến. Đáng chú ý, mọi người đề xuất cách tiếp cận này ngay cả trong các ngôn ngữ OO truyền thống, như C #, Java hoặc C ++, không chỉ trong các ngôn ngữ chức năng thuần túy như Haskell buộc điều này phải lập trình viên.

Tôi thấy khó hiểu, vì tôi thấy tính dễ thay đổi và tác dụng phụ ... thuận tiện. Tuy nhiên, do cách mọi người hiện lên án các tác dụng phụ và coi đó là một cách tốt để loại bỏ chúng bất cứ khi nào có thể, tôi tin rằng nếu tôi muốn trở thành một lập trình viên có năng lực, tôi phải bắt đầu luật sư của mình để hiểu rõ hơn về mô hình ... Do đó Q. của tôi

Một nơi khi tôi tìm thấy vấn đề với mô hình chức năng là khi một đối tượng được tham chiếu tự nhiên từ nhiều nơi. Hãy để tôi mô tả nó trên hai ví dụ.

Ví dụ đầu tiên sẽ là trò chơi C # của tôi, tôi đang cố gắng thực hiện trong thời gian rảnh rỗi . Đây là trò chơi web theo lượt, trong đó cả hai người chơi có đội gồm 4 quái vật và có thể gửi một quái vật từ đội của họ đến chiến trường, nơi nó sẽ đối mặt với quái vật được gửi bởi người chơi đối phương. Người chơi cũng có thể nhớ lại quái vật từ chiến trường và thay thế chúng bằng một quái vật khác từ đội của họ (tương tự như Pokemon).

Trong cài đặt này, một quái vật duy nhất có thể được tham chiếu tự nhiên từ ít nhất 2 nơi: đội của người chơi và chiến trường, nơi tham chiếu hai quái vật "hoạt động".

Bây giờ hãy xem xét tình huống khi một con quái vật bị tấn công và mất 20 điểm máu. Trong dấu ngoặc của mô hình bắt buộc, tôi sửa đổi healthtrường của quái vật này để phản ánh sự thay đổi này - và đây là những gì tôi đang làm bây giờ. Tuy nhiên, điều này làm cho Monsterlớp có thể thay đổi và các hàm (phương thức) liên quan không tinh khiết, mà tôi đoán được coi là một thực tiễn xấu cho đến nay.

Mặc dù tôi đã cho phép mình có mã của trò chơi này ở trạng thái không lý tưởng để có bất kỳ hy vọng nào thực sự hoàn thành nó vào một lúc nào đó trong tương lai, tôi muốn biết và hiểu nó nên như thế nào viết đúng. Do đó: Nếu đây là một lỗi thiết kế, làm thế nào để khắc phục nó?

Theo kiểu chức năng, như tôi hiểu, thay vào đó tôi sẽ tạo một bản sao của Monsterđối tượng này , giữ cho nó giống hệt với cái cũ ngoại trừ trường này; và phương thức suffer_hitsẽ trả về đối tượng mới này thay vì sửa đổi đối tượng cũ tại chỗ. Sau đó, tôi cũng sao chép Battlefieldđối tượng, giữ tất cả các trường của nó giống nhau ngoại trừ con quái vật này.

Điều này đi kèm với ít nhất 2 khó khăn:

  1. Hệ thống phân cấp có thể dễ dàng sâu hơn nhiều so với ví dụ đơn giản này chỉ Battlefield-> Monster. Tôi phải thực hiện sao chép tất cả các trường trừ một trường và trả lại một đối tượng mới theo cách phân cấp này. Đây sẽ là mã soạn sẵn mà tôi thấy khó chịu, đặc biệt là khi lập trình chức năng được cho là làm giảm bản ghi.
  2. Tuy nhiên, một vấn đề nghiêm trọng hơn nhiều là điều này sẽ dẫn đến việc dữ liệu không đồng bộ . Quái vật hoạt động của lĩnh vực này sẽ thấy sức khỏe của nó giảm đi; tuy nhiên, cùng một con quái vật, được tham chiếu từ người chơi điều khiển của nó Team, thì không. Thay vào đó, nếu tôi chấp nhận phong cách bắt buộc, mọi sửa đổi dữ liệu sẽ được hiển thị ngay lập tức từ tất cả các vị trí mã khác và trong những trường hợp như thế này tôi thấy nó thực sự tiện lợi - nhưng cách tôi nhận được mọi thứ chính xác là những gì mọi người nói là sai với phong cách cấp bách!
    • Bây giờ có thể xử lý vấn đề này bằng cách thực hiện một hành trình đến Teamsau mỗi cuộc tấn công. Đây là công việc làm thêm. Tuy nhiên, điều gì sẽ xảy ra nếu một con quái vật đột nhiên có thể được tham chiếu sau đó từ nhiều nơi hơn? Điều gì sẽ xảy ra nếu tôi đi kèm với một khả năng, ví dụ, cho phép một con quái vật tập trung vào một con quái vật khác không nhất thiết phải ở trên sân (tôi thực sự đang xem xét một khả năng như vậy)? Tôi chắc chắn sẽ nhớ một chuyến đi đến những con quái vật tập trung ngay sau mỗi cuộc tấn công chứ? Đây dường như là một quả bom hẹn giờ sẽ phát nổ khi mã trở nên phức tạp hơn, vì vậy tôi nghĩ đó không phải là giải pháp.

Một ý tưởng cho một giải pháp tốt hơn xuất phát từ ví dụ thứ hai của tôi, khi tôi gặp vấn đề tương tự. Trong giới hàn lâm, chúng tôi được yêu cầu viết một thông dịch viên về ngôn ngữ của thiết kế của chúng tôi tại Haskell. (Đây cũng là cách tôi buộc phải bắt đầu hiểu FP là gì). Vấn đề xuất hiện khi tôi đang thực hiện đóng cửa. Một lần nữa, cùng một phạm vi có thể được tham chiếu từ nhiều nơi: Thông qua biến chứa phạm vi này là phạm vi chính của bất kỳ phạm vi lồng nhau nào! Rõ ràng, nếu một thay đổi được thực hiện cho phạm vi này thông qua bất kỳ tham chiếu nào trỏ đến nó, thay đổi này cũng phải được hiển thị thông qua tất cả các tham chiếu khác.

Giải pháp tôi đưa ra là gán cho mỗi phạm vi một ID và giữ một từ điển trung tâm của tất cả các phạm vi trong Stateđơn nguyên. Bây giờ các biến sẽ chỉ giữ ID của phạm vi mà chúng bị ràng buộc, thay vì chính phạm vi đó và các phạm vi lồng nhau cũng sẽ giữ ID của phạm vi cha của chúng.

Tôi đoán cách tiếp cận tương tự có thể được thử trong trò chơi chiến đấu với quái vật của tôi ... Các trường và các đội không tham chiếu quái vật; thay vào đó họ giữ ID của quái vật được lưu trong từ điển quái vật trung tâm.

Tuy nhiên, một lần nữa tôi có thể thấy một vấn đề với cách tiếp cận này khiến tôi không thể chấp nhận nó mà không do dự là giải pháp cho vấn đề:

Nó một lần nữa là một nguồn của mã soạn sẵn. Nó tạo ra một lớp nhất thiết phải là 3 lớp: điều trước đây là sửa đổi tại chỗ một dòng của một trường duy nhất bây giờ yêu cầu (a) Lấy đối tượng từ từ điển trung tâm (b) Thực hiện thay đổi (c) Lưu đối tượng mới vào từ điển trung tâm. Ngoài ra, việc giữ id của các đối tượng và từ điển trung tâm thay vì có các tài liệu tham khảo làm tăng sự phức tạp. Vì FP được quảng cáo để giảm độ phức tạp và mã soạn sẵn, nên gợi ý này là tôi đã làm sai.

Tôi cũng sẽ viết về vấn đề thứ hai có vẻ nghiêm trọng hơn nhiều: Cách tiếp cận này giới thiệu rò rỉ bộ nhớ . Các đối tượng không thể truy cập thường sẽ là rác được thu thập. Tuy nhiên, các đối tượng được giữ trong một từ điển trung tâm không thể được thu gom rác, ngay cả khi không có đối tượng có thể truy cập tham chiếu ID cụ thể này. Và trong khi về mặt lý thuyết, lập trình cẩn thận có thể tránh rò rỉ bộ nhớ (chúng tôi có thể cẩn thận xóa từng đối tượng khỏi từ điển trung tâm một khi không còn cần thiết), đây là lỗi dễ xảy ra và FP được quảng cáo để tăng tính chính xác của các chương trình. không phải là cách chính xác

Tuy nhiên, tôi đã phát hiện ra rằng nó dường như là một vấn đề được giải quyết. Java cung cấp WeakHashMapcó thể được sử dụng để giải quyết vấn đề này. C # cung cấp một cơ sở tương tự - ConditionalWeakTable- mặc dù theo các tài liệu, nó có nghĩa là được sử dụng bởi các trình biên dịch. Và trong Haskell, chúng ta có System.Mem.Weak .

Việc lưu trữ các từ điển như vậy có phải là giải pháp chức năng chính xác cho vấn đề này hay có một cách đơn giản hơn mà tôi không thấy? Tôi tưởng tượng rằng số lượng từ điển như vậy có thể dễ dàng phát triển và xấu; Vì vậy, nếu những từ điển này được coi là bất biến thì điều này có thể có nghĩa là rất nhiều thông số truyền hoặc trong các ngôn ngữ hỗ trợ tính toán đơn âm, vì từ điển sẽ được giữ trong các đơn nguyên (nhưng một lần nữa tôi lại đọc rằng nó hoàn toàn có chức năng các ngôn ngữ càng ít mã càng tốt, nên trong khi giải pháp từ điển này sẽ đặt gần như tất cả mã bên trong Stateđơn nguyên, điều này một lần nữa khiến tôi nghi ngờ nếu đây là giải pháp chính xác.)

Sau khi xem xét, tôi nghĩ rằng tôi sẽ thêm một câu hỏi nữa: Chúng ta đạt được gì khi xây dựng những từ điển như vậy? Theo các chuyên gia, điều không đúng với lập trình mệnh lệnh là, những thay đổi trong một số đối tượng lan truyền sang các đoạn mã khác. Để giải quyết vấn đề này, các đối tượng được cho là bất biến - chính xác là vì lý do này, nếu tôi hiểu chính xác, những thay đổi được thực hiện đối với chúng không nên được nhìn thấy ở nơi khác. Nhưng bây giờ tôi lo ngại về các đoạn mã khác hoạt động trên dữ liệu lỗi thời vì vậy tôi phát minh ra từ điển trung tâm để ... một lần nữa thay đổi trong một số đoạn mã truyền sang các đoạn mã khác! Do đó, chúng ta không trở lại phong cách cấp bách với tất cả những nhược điểm được cho là của nó, nhưng với sự phức tạp thêm vào?


6
Để đưa ra quan điểm này, các chương trình bất biến chức năng chủ yếu dành cho các tình huống xử lý dữ liệu liên quan đến đồng thời. Nói cách khác, các chương trình xử lý dữ liệu đầu vào thông qua một bộ phương trình hoặc quy trình tạo ra kết quả đầu ra. Tính không thay đổi giúp trong kịch bản này vì một số lý do: các giá trị được đọc bởi nhiều luồng được đảm bảo không thay đổi trong suốt vòng đời của chúng, điều này giúp đơn giản hóa rất nhiều khả năng xử lý dữ liệu theo cách không khóa và lý do về cách thức hoạt động của thuật toán.
Robert Harvey

8
Bí mật bẩn thỉu về sự bất biến chức năng và lập trình trò chơi là hai thứ đó không tương thích với nhau. Về cơ bản, bạn đang cố gắng mô hình hóa một hệ thống năng động, luôn thay đổi bằng cách sử dụng cấu trúc dữ liệu tĩnh, bất động.
Robert Harvey

2
Đừng coi sự biến đổi so với sự bất biến như một giáo điều tôn giáo. Có những tình huống mà mỗi cái tốt hơn cái kia, tính bất biến không phải lúc nào cũng tốt hơn, ví dụ viết một bộ công cụ GUI với các kiểu dữ liệu bất biến sẽ là một cơn ác mộng tuyệt đối.
whatsisname

1
Câu hỏi cụ thể C # này và các câu trả lời của nó bao gồm vấn đề về nồi hơi, chủ yếu xuất phát từ nhu cầu tạo các bản sao được sửa đổi một chút (cập nhật) của một đối tượng bất biến hiện có.
rwong

2
Một cái nhìn sâu sắc quan trọng là một con quái vật trong trò chơi này được coi là một thực thể. Ngoài ra, kết quả của mỗi trận chiến (bao gồm số thứ tự trận chiến, ID thực thể của quái vật, trạng thái của quái vật trước và sau trận chiến) được coi là một trạng thái tại một thời điểm nhất định (hoặc bước thời gian). Do đó, người chơi ( Team) có thể truy xuất kết quả của trận chiến và do đó các trạng thái của quái vật bằng một tuple (số chiến đấu, ID thực thể quái vật).
rwong

Câu trả lời:


19

Làm thế nào để lập trình chức năng xử lý một đối tượng được tham chiếu từ nhiều nơi? Nó mời bạn xem lại mô hình của bạn!

Để giải thích ... chúng ta hãy xem cách các trò chơi nối mạng đôi khi được viết - với một bản sao "nguồn vàng" trung tâm của trạng thái trò chơi và một tập hợp các sự kiện khách đến cập nhật trạng thái đó, sau đó được phát lại cho các khách hàng khác .

Bạn có thể đọc về niềm vui mà nhóm Factorio có được khi điều này hoạt động tốt trong một số tình huống; Dưới đây là một tổng quan ngắn về mô hình của họ:

Cách cơ bản mà nhiều người chơi của chúng tôi hoạt động là tất cả các máy khách mô phỏng trạng thái trò chơi và họ chỉ nhận và gửi đầu vào của người chơi (được gọi là Hành động nhập). Trách nhiệm chính của máy chủ là ủy quyền các Tác vụ nhập liệu và đảm bảo tất cả các máy khách thực hiện cùng một hành động trong cùng một đánh dấu.

Vì máy chủ cần phân xử khi các hành động được thực thi, một hành động của người chơi sẽ di chuyển một thứ như thế này: Hành động của người chơi -> Máy khách trò chơi -> Mạng -> Máy chủ -> Mạng-> Máy khách trò chơi. Điều này có nghĩa là mọi hành động của người chơi chỉ được thực hiện khi nó thực hiện một chuyến đi khứ hồi qua mạng. Điều này sẽ làm cho trò chơi cảm thấy thực sự lag, đó là lý do tại sao ẩn độ trễ là một cơ chế được thêm vào trong trò chơi gần như kể từ khi giới thiệu nhiều người chơi. Ẩn độ trễ hoạt động bằng cách mô phỏng đầu vào của người chơi, mà không xem xét hành động của những người chơi khác và không xem xét sự tùy tiện của máy chủ.

Trong Factorio chúng ta có Trạng thái trò chơi, đây là trạng thái đầy đủ của bản đồ, người chơi, quyền lợi, mọi thứ. Nó được mô phỏng xác định trên tất cả các máy khách dựa trên các hành động nhận được từ máy chủ. Điều này là thiêng liêng và nếu nó khác với máy chủ hoặc bất kỳ máy khách nào khác, một sự không đồng bộ xảy ra.

Trên đầu Bang trò chơi, chúng tôi có Trạng thái trễ. Điều này chứa một tập hợp nhỏ của trạng thái chính. Trạng thái trễ không phải là thiêng liêng và nó chỉ đại diện cho cách chúng ta nghĩ trạng thái trò chơi sẽ như thế nào trong tương lai dựa trên các Tác vụ nhập liệu mà người chơi thực hiện.

Điều quan trọng là trạng thái của từng đối tượng là bất biến tại dấu tích cụ thể trong dòng thời gian . Tất cả mọi thứ trong trạng thái nhiều người chơi toàn cầu cuối cùng phải hội tụ đến một thực tế xác định.

Và - đó có thể là chìa khóa cho câu hỏi của bạn. Mỗi trạng thái của thực thể là bất biến đối với một đánh dấu nhất định và bạn theo dõi các sự kiện chuyển tiếp tạo ra các thể hiện mới theo thời gian.

Nếu bạn nghĩ về nó, hàng đợi sự kiện đến từ máy chủ phải có quyền truy cập vào một thư mục trung tâm của các thực thể, để nó có thể áp dụng các sự kiện của nó.

Cuối cùng, các phương thức trình biến đổi một dòng đơn giản mà bạn không muốn phức tạp hóa chỉ đơn giản vì bạn không thực sự mô hình hóa thời gian chính xác. Xét cho cùng , nếu sức khỏe có thể thay đổi ở giữa vòng xử lý, thì các thực thể trước đó trong dấu kiểm này sẽ thấy một giá trị cũ và những thực thể sau đó sẽ thấy một giá trị thay đổi. Quản lý điều này một cách cẩn thận có nghĩa là ở các trạng thái khác biệt nhất hiện tại (bất biến) và tiếp theo (đang xây dựng), thực sự chỉ là hai tích tắc trong dòng thời gian tuyệt vời của bọ ve!

Vì vậy, như một hướng dẫn rộng, hãy xem xét việc phá vỡ trạng thái của quái vật thành một số vật thể nhỏ có liên quan đến vị trí / vận tốc / vật lý, sức khỏe / thiệt hại, tài sản. Xây dựng một sự kiện để mô tả từng đột biến có thể xảy ra và chạy vòng lặp chính của bạn dưới dạng:

  1. xử lý đầu vào và tạo các sự kiện tương ứng
  2. tạo các sự kiện nội bộ (ví dụ do va chạm đối tượng, v.v.)
  3. áp dụng các sự kiện cho các quái vật bất biến hiện tại, để tạo ra các quái vật mới cho lần đánh dấu tiếp theo - chủ yếu là sao chép trạng thái không thay đổi cũ nếu có thể, nhưng tạo các đối tượng trạng thái mới khi cần thiết.
  4. kết xuất và lặp lại cho đánh dấu tiếp theo.

Hoặc điều tương tự. Tôi thấy suy nghĩ "làm thế nào để tôi phân phối cái này?" nói chung là một bài tập tinh thần khá tốt, để tinh chỉnh sự hiểu biết của tôi khi tôi bối rối về nơi mọi thứ sống và cách chúng nên phát triển.

Nhờ một ghi chú từ @ AaronM.Eshbach, nhấn mạnh rằng đây là miền có vấn đề tương tự với Nguồn sự kiện và mẫu CQRS , nơi bạn đang mô hình hóa các thay đổi thành trạng thái trong một hệ thống phân tán dưới dạng một loạt các sự kiện bất biến theo thời gian . Trong trường hợp này, rất có thể chúng tôi đang cố gắng dọn sạch một ứng dụng cơ sở dữ liệu phức tạp, bằng cách tách biệt (như tên cho thấy!) Việc xử lý lệnh của trình biến đổi từ hệ thống truy vấn / xem. Tất nhiên phức tạp hơn, nhưng linh hoạt hơn.


2
Để tham khảo thêm, xem Nguồn tìm sự kiệnCQRS . Đây là một miền vấn đề tương tự: Mô hình hóa các thay đổi thành trạng thái trong một hệ thống phân tán dưới dạng một chuỗi các sự kiện bất biến theo thời gian.
Aaron M. Eshbach

@ AaronM.Eshbach đó là một! Bạn có phiền nếu tôi đưa bình luận / trích dẫn của bạn vào câu trả lời không? Nó làm cho nó âm thanh có thẩm quyền hơn. Cảm ơn!
SusanW

Tất nhiên là không, xin vui lòng làm.
Aaron M. Eshbach

3

Bạn vẫn còn một nửa trong trại bắt buộc. Thay vì nghĩ về một đối tượng tại một thời điểm, hãy nghĩ về trò chơi của bạn về mặt lịch sử của các vở kịch hoặc sự kiện

p1 - send m1 to battlefield
p2 - send m2 to battlefield
m1 - attacks m2 (2 dam)
m2 - attacks m1 (10 dam)
p1 - retreats m1

Vân vân

Bạn có thể tính toán trạng thái của trò chơi tại bất kỳ điểm nào bằng cách kết hợp các hành động lại với nhau để tạo ra một đối tượng trạng thái bất biến. Mỗi lần phát là một hàm lấy một đối tượng trạng thái và trả về một đối tượng trạng thái mới

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.