Cấu trúc bất biến và phân cấp thành phần sâu sắc


9

Tôi đang phát triển một ứng dụng GUI, hoạt động rất nhiều với đồ họa - bạn có thể nghĩ về nó như một trình soạn thảo vector, vì ví dụ này. Sẽ rất hấp dẫn khi làm cho tất cả các cấu trúc dữ liệu trở nên bất biến - vì vậy tôi có thể hoàn tác / làm lại, sao chép / dán và nhiều thứ khác gần như không cần nỗ lực.

Để đơn giản hóa, tôi sẽ sử dụng ví dụ sau - ứng dụng được sử dụng để chỉnh sửa hình dạng đa giác, vì vậy tôi có đối tượng "Đa giác", đơn giản là danh sách các điểm bất biến:

Scene -> Polygon -> Point

Và vì vậy tôi chỉ có một biến có thể thay đổi trong chương trình của mình - biến chứa đối tượng Cảnh hiện tại. Vấn đề mà tôi gặp phải khi tôi cố gắng thực hiện kéo điểm - trong phiên bản có thể thay đổi, tôi chỉ cần lấy một Pointđối tượng và bắt đầu sửa đổi tọa độ của nó. Trong phiên bản bất biến - tôi bị mắc kẹt. Tôi có thể đã lưu trữ các chỉ số Polygonhiện tại Scene, chỉ mục của điểm được kéo vào Polygonvà thay thế nó mỗi lần. Nhưng cách tiếp cận này không mở rộng quy mô - khi mức độ thành phần lên đến 5 và hơn nữa, nồi hơi sẽ trở nên không thể chịu đựng được.

Tôi chắc chắn rằng vấn đề này có thể được giải quyết - sau tất cả, có Haskell với các cấu trúc hoàn toàn bất biến và đơn nguyên IO. Nhưng tôi không thể tìm thấy làm thế nào.

Bạn có thể cho tôi một gợi ý?


@Job - đó là cách nó hoạt động ngay bây giờ, và nó mang lại cho tôi nhiều nỗi đau. Vì vậy, tôi đang tìm kiếm các phương pháp thay thế - và tính bất biến có vẻ hoàn hảo cho cấu trúc ứng dụng này, ít nhất là trước khi chúng tôi thêm tương tác người dùng vào nó :)
Rogach

@Rogach: Bạn có thể giải thích thêm về mã soạn sẵn của mình không?
rwong

Câu trả lời:


9

Tôi có thể đã lưu trữ các chỉ mục của Đa giác trong Cảnh hiện tại, chỉ mục của điểm được kéo trong Đa giác và thay thế nó mỗi lần. Nhưng cách tiếp cận này không mở rộng quy mô - khi mức độ thành phần lên đến 5 và hơn nữa, nồi hơi sẽ trở nên không thể chịu đựng được.

Bạn hoàn toàn đúng, cách tiếp cận này không mở rộng nếu bạn không thể đi loanh quanh . Cụ thể, bản tóm tắt để tạo Cảnh hoàn toàn mới với một phần phụ nhỏ đã thay đổi. Tuy nhiên, nhiều ngôn ngữ chức năng cung cấp một cấu trúc để xử lý loại thao tác cấu trúc lồng nhau này: ống kính.

Một ống kính về cơ bản là một getter và setter cho dữ liệu bất biến. Một ống kính tập trung vào một số phần nhỏ của cấu trúc lớn hơn. Với một ống kính, có hai điều bạn có thể làm với nó - bạn có thể xem phần nhỏ của giá trị của cấu trúc lớn hơn hoặc bạn có thể đặt phần nhỏ của giá trị của cấu trúc lớn hơn thành giá trị mới. Ví dụ: giả sử bạn có một ống kính tập trung vào mục thứ ba trong danh sách:

thirdItemLens :: Lens [a] a

Kiểu đó có nghĩa là cấu trúc lớn hơn là một danh sách các thứ và phần nhỏ là một trong những thứ đó. Với ống kính này, bạn có thể xem và đặt mục thứ ba trong danh sách:

> view thirdItemLens [1, 2, 3, 4, 5]
3
> set thirdItemLens 100 [1, 2, 3, 4, 5]
[1, 2, 100, 4, 5]

Các ống kính lý do là hữu ích là bởi vì chúng là các giá trị đại diện cho getters và setters và bạn có thể trừu tượng hóa chúng giống như cách bạn có thể các giá trị khác. Bạn có thể tạo các hàm trả về ống kính, ví dụ như listItemLenshàm lấy số nvà trả về ống kính xem nmục thứ trong danh sách. Ngoài ra, ống kính có thể được sáng tác :

> firstLens = listItemLens 0
> thirdLens = listItemLens 2
> firstOfThirdLens = lensCompose firstLens thirdLens
> view firstOfThirdLens [[1, 2], [3, 4], [5, 6], [7, 8]]
5
> set firstOfThirdLens 100 [[1, 2], [3, 4], [5, 6], [7, 8]]
[[1, 2], [3, 4], [100, 6], [7, 8]]

Mỗi ống kính đóng gói hành vi để vượt qua một cấp của cấu trúc dữ liệu. Bằng cách kết hợp chúng, bạn có thể loại bỏ bản tóm tắt để vượt qua nhiều cấp cấu trúc phức tạp. Chẳng hạn, giả sử bạn có một scenePolygonLens ichế độ xem iĐa giác thứ trong Cảnh và một polygonPointLens nđiểm xem nthĐiểm trong Đa giác, bạn có thể tạo một nhà xây dựng ống kính để chỉ tập trung vào điểm cụ thể mà bạn quan tâm trong toàn bộ cảnh như vậy:

scenePointLens i n = lensCompose (polygonPointLens n) (scenePolygonLens i)

Bây giờ, giả sử người dùng nhấp vào điểm 3 của đa giác 14 và di chuyển nó đúng 10 pixel. Bạn có thể cập nhật cảnh của mình như vậy:

lens = scenePointLens 14 3
point = view lens currentScene
newPoint = movePoint 10 0 point
newScene = set lens newPoint currentScene

Điều này độc đáo chứa tất cả các mẫu soạn thảo để duyệt và cập nhật Cảnh bên trong lens, tất cả những gì bạn phải quan tâm là những gì bạn muốn thay đổi điểm thành. Bạn có thể trừu tượng hóa điều này bằng một lensTransformchức năng chấp nhận ống kính, mục tiêu và chức năng cập nhật chế độ xem của mục tiêu thông qua ống kính:

lensTransform lens transformFunc target =
  current = view lens target
  new = transformFunc current
  set lens new target

Điều này có một chức năng và biến nó thành một "trình cập nhật" trên một cấu trúc dữ liệu phức tạp, áp dụng chức năng này chỉ cho chế độ xem và sử dụng nó để xây dựng một chế độ xem mới. Vì vậy, quay trở lại kịch bản di chuyển điểm thứ 3 của đa giác thứ 14 sang 10 pixel bên phải, có thể được biểu thị theo cách lensTransformtương tự như vậy:

lens = scenePointLens 14 3
moveRightTen point = movePoint 10 0 point
newScene = lensTransform lens moveRightTen currentScene

Và đó là tất cả những gì bạn cần để cập nhật toàn bộ khung cảnh. Đây là một ý tưởng rất mạnh mẽ và hoạt động rất tốt khi bạn có một số chức năng tốt để xây dựng các ống kính xem các phần dữ liệu bạn quan tâm.

Tuy nhiên đây là tất cả những thứ khá hiện có, ngay cả trong cộng đồng lập trình chức năng. Thật khó để tìm thấy sự hỗ trợ thư viện tốt để làm việc với ống kính, và thậm chí còn khó khăn hơn để giải thích cách chúng hoạt động và những lợi ích cho đồng nghiệp của bạn. Thực hiện phương pháp này với một hạt muối.


Giải thích tuyệt vời! Bây giờ tôi có được ống kính là gì!
Vincent Lecrubier 22/03/2016

13

Tôi đã làm việc với chính xác cùng một vấn đề (nhưng chỉ với 3 cấp độ thành phần). Ý tưởng cơ bản là nhân bản, sau đó sửa đổi . Trong phong cách lập trình bất biến, việc nhân bản và sửa đổi phải xảy ra cùng nhau, trở thành đối tượng chỉ huy .

Lưu ý rằng trong phong cách lập trình có thể thay đổi, dù sao cũng cần phải nhân bản:

  • Để cho phép hoàn tác / làm lại
  • Hệ thống hiển thị có thể cần hiển thị đồng thời mô hình "trước khi chỉnh sửa" và "trong khi chỉnh sửa", chồng chéo (dưới dạng dòng ma), để người dùng có thể thấy các thay đổi.

Trong phong cách lập trình đột biến,

  • Cấu trúc hiện tại được nhân bản sâu
  • Những thay đổi được thực hiện trong bản sao
  • Công cụ hiển thị được yêu cầu kết xuất cấu trúc cũ theo dòng ma và cấu trúc nhân bản / sửa đổi có màu.

Trong phong cách lập trình bất biến,

  • Mỗi hành động của người dùng dẫn đến sửa đổi dữ liệu được ánh xạ tới một chuỗi các "lệnh".
  • Một đối tượng lệnh đóng gói chính xác những gì sửa đổi sẽ được áp dụng và tham chiếu đến cấu trúc ban đầu.
    • Trong trường hợp của tôi, đối tượng lệnh của tôi chỉ nhớ chỉ số điểm cần thay đổi và tọa độ mới. (tức là rất nhẹ, vì tôi không tuân theo phong cách bất biến.)
  • Khi một đối tượng lệnh được thực thi, nó tạo ra một bản sao sâu đã sửa đổi của cấu trúc, làm cho sửa đổi vĩnh viễn trong bản sao mới.
  • Khi người dùng thực hiện nhiều chỉnh sửa, nhiều đối tượng lệnh sẽ được tạo.

1
Tại sao tạo một bản sao sâu của cấu trúc dữ liệu bất biến? Bạn chỉ cần sao chép "cột sống" của các tham chiếu từ đối tượng đã sửa đổi vào thư mục gốc và giữ lại các tham chiếu đến các phần còn lại của cấu trúc ban đầu.
Phục hồi Monica

3

Các đối tượng sâu không thay đổi có lợi thế là nhân bản sâu một cái gì đó đơn giản chỉ cần sao chép một tài liệu tham khảo. Họ có nhược điểm là thực hiện ngay cả một thay đổi nhỏ đối với một đối tượng được lồng sâu đòi hỏi phải xây dựng một thể hiện mới của mọi đối tượng trong đó nó được lồng vào nhau. Các đối tượng có thể thay đổi có lợi thế là việc thay đổi một đối tượng rất dễ dàng - chỉ cần thực hiện - nhưng nhân bản sâu một đối tượng đòi hỏi phải xây dựng một đối tượng mới chứa bản sao sâu của mọi đối tượng lồng nhau. Tệ hơn nữa, nếu ai muốn sao chép một đối tượng và thực hiện thay đổi, tạo bản sao đối tượng, thực hiện thay đổi khác, vv sau đó không có vấn đề như thế nào dù lớn hay nhỏ những thay đổi là người ta phải giữ một bản sao của toàn bộ hệ thống phân cấp cho mỗi phiên bản lưu của trạng thái của đối tượng. Bẩn thỉu.

Một cách tiếp cận có thể đáng xem xét sẽ là định nghĩa một loại "có thể thay đổi" trừu tượng với các loại dẫn xuất có thể thay đổi và không thay đổi sâu sắc. Tất cả các loại như vậy sẽ có một AsImmutablephương pháp; việc gọi phương thức đó trong một thể hiện không thay đổi sâu sắc của một đối tượng sẽ chỉ trả về thể hiện đó. Gọi nó trên một thể hiện có thể thay đổi sẽ trả về một thể hiện bất biến sâu sắc có các thuộc tính là các ảnh chụp nhanh bất biến của các tương đương của chúng trong bản gốc. Các loại không thay đổi có tương đương có thể thay đổi sẽ tạo ra một AsMutablephương thức, trong đó sẽ xây dựng một thể hiện có thể thay đổi có các thuộc tính khớp với các thuộc tính ban đầu.

Thay đổi một đối tượng lồng nhau trong một đối tượng bất biến sâu sẽ yêu cầu trước tiên thay thế đối tượng bất biến bên ngoài bằng một đối tượng có thể thay đổi, sau đó thay thế thuộc tính chứa vật thể được thay đổi bằng một đối tượng có thể thay đổi, v.v. tổng thể đối tượng sẽ không yêu cầu tạo thêm bất kỳ đối tượng nào cho đến khi cố gắng gọi AsImmutablemột đối tượng có thể thay đổi (điều này sẽ khiến các đối tượng có thể thay đổi có thể thay đổi, nhưng trả lại các đối tượng không thay đổi giữ cùng một dữ liệu).

Là tối ưu hóa đơn giản nhưng có ý nghĩa, mỗi đối tượng có thể thay đổi có thể chứa một tham chiếu được lưu trong bộ nhớ cache đến một đối tượng thuộc loại không thay đổi được liên kết và mỗi loại không thay đổi sẽ lưu trữ GetHashCodegiá trị của nó . Khi gọi AsImmutablemột đối tượng có thể thay đổi, trước khi trả về một đối tượng bất biến mới, hãy kiểm tra xem nó có khớp với tham chiếu được lưu trữ không. Nếu vậy, trả về tham chiếu được lưu trong bộ nhớ cache (từ bỏ đối tượng bất biến mới). Nếu không thì cập nhật tham chiếu đã lưu vào bộ nhớ cache để giữ đối tượng mới và trả về đó. Nếu điều này được thực hiện, lặp lại các cuộc gọi đếnAsImmutablekhông có bất kỳ đột biến can thiệp nào sẽ mang lại các tham chiếu đối tượng giống nhau. Ngay cả khi người ta không tiết kiệm chi phí xây dựng các thể hiện mới, người ta sẽ tránh được chi phí bộ nhớ để giữ chúng. Hơn nữa, so sánh bình đẳng giữa các đối tượng bất biến có thể được tiến hành rất nhiều nếu trong hầu hết các trường hợp, các mục được so sánh là bằng tham chiếu hoặc có mã băm khác nhau.

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.