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 health
trườ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 Monster
lớ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_hit
sẽ 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:
- 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. - 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
Team
sau 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.
- 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
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 và 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 WeakHashMap
có 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?
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).