Làm thế nào để ngôn ngữ lập trình chức năng thuần túy đối phó với dữ liệu thay đổi nhanh chóng?


22

Những cấu trúc dữ liệu nào bạn có thể sử dụng để bạn có thể loại bỏ và thay thế O (1)? Hoặc làm thế nào bạn có thể tránh các tình huống khi bạn cần cấu trúc nói?


2
Đối với những người trong chúng ta ít quen thuộc với các ngôn ngữ lập trình chức năng thuần túy, bạn có gầy không, bạn có thể cung cấp thêm một chút nền tảng để chúng tôi hiểu vấn đề của bạn là gì không?
Thất

4
@FrustratedWithFormsDesigner Các ngôn ngữ lập trình chức năng thuần túy yêu cầu tất cả các biến là bất biến, do đó đòi hỏi các cấu trúc dữ liệu tạo ra các phiên bản mới của chính chúng khi được "sửa đổi".
Doval

5
Bạn có biết công việc của Okasaki trên các cấu trúc dữ liệu hoàn toàn chức năng không?

2
Một khả năng là xác định một đơn nguyên cho dữ liệu có thể thay đổi (xem ví dụ haskell.org/ghc/docs/4.08/set/sec-marray.html ). Theo cách này, dữ liệu có thể thay đổi được xử lý tương tự như IO.
Giorgio

1
@CodesInChaos: tuy nhiên, các cấu trúc bất biến như vậy thường có chi phí hoạt động cao hơn nhiều so với các mảng đơn giản. Kết quả là, có rất nhiều sự khác biệt thực tế. Đó là lý do tại sao bất kỳ ngôn ngữ chức năng thuần túy nào nhắm vào lập trình mục đích chung nên có cách sử dụng các cấu trúc có thể thay đổi, theo cách nào đó an toàn tương thích với ngữ nghĩa thuần túy. Các STđơn vị trong Haskell thực hiện điều này một cách xuất sắc.
vòng xoay trái

Câu trả lời:


32

Có một loạt các cấu trúc dữ liệu khai thác sự lười biếng và các thủ thuật khác để đạt được thời gian cố định hoặc thậm chí (đối với một số trường hợp hạn chế, chẳng hạn như hàng đợi ) cập nhật thời gian liên tục cho nhiều loại vấn đề. Luận án tiến sĩ "Cấu trúc dữ liệu chức năng thuần túy" của Chris Okasaki và cuốn sách cùng tên là một ví dụ điển hình (có lẽ là cuốn chính đầu tiên), nhưng lĩnh vực này đã phát triển kể từ đó . Các cấu trúc dữ liệu này thường không chỉ đơn thuần là chức năng trong giao diện, mà còn có thể được thực hiện bằng Haskell thuần túy và các ngôn ngữ tương tự, và hoàn toàn bền bỉ.

Ngay cả khi không có bất kỳ công cụ tiên tiến nào, các cây tìm kiếm nhị phân cân bằng đơn giản cung cấp các cập nhật theo thời gian logarit, do đó bộ nhớ có thể thay đổi có thể được mô phỏng với điều tồi tệ nhất là làm chậm logarit.

Có những lựa chọn khác, có thể được coi là gian lận, nhưng rất hiệu quả liên quan đến nỗ lực thực hiện và hiệu suất trong thế giới thực. Ví dụ, các loại tuyến tính hoặc các loại duy nhất cho phép cập nhật tại chỗ làm chiến lược triển khai cho ngôn ngữ thuần túy về mặt khái niệm, bằng cách ngăn chương trình giữ giá trị trước đó (bộ nhớ sẽ bị thay đổi). Đây là ít chung hơn so với cấu trúc dữ liệu liên tục: Ví dụ: bạn không thể dễ dàng xây dựng nhật ký hoàn tác bằng cách lưu trữ tất cả các phiên bản trước đó của trạng thái. Nó vẫn là một công cụ mạnh mẽ, mặc dù AFAIK chưa có sẵn trong các ngôn ngữ chức năng chính.

Một tùy chọn khác để giới thiệu trạng thái có thể thay đổi một cách an toàn vào một cài đặt chức năng là STđơn nguyên trong Haskell. Nó có thể được thực hiện mà không bị đột biến và chặn các unsafe*chức năng, nó hoạt động như thể nó chỉ là một trình bao bọc ưa thích xung quanh việc chuyển một cấu trúc dữ liệu liên tục ngầm (xem State). Nhưng do một số mánh khóe hệ thống thực thi lệnh đánh giá và ngăn chặn thoát, nó có thể được thực hiện một cách an toàn với đột biến tại chỗ, với tất cả các lợi ích hiệu suất.


Cũng có thể đáng nói đến Dây khóa kéo cho bạn khả năng thực hiện các thay đổi nhanh chóng tại một tiêu điểm trong danh sách hoặc cây
jk.

1
@jk. Chúng được đề cập trong bài đăng Khoa học máy tính lý thuyết mà tôi liên kết đến. Hơn nữa, chúng chỉ là một (tốt, một lớp) gồm nhiều cấu trúc dữ liệu có liên quan và thảo luận về tất cả chúng nằm ngoài phạm vi và ít được sử dụng.

đủ công bằng, đã không theo các liên kết
jk.

9

Một cấu trúc có thể thay đổi giá rẻ là ngăn xếp đối số.

Hãy xem cách tính giai thừa theo kiểu SICP điển hình:

(defn fac (n accum) 
    (if (= n 1) 
        accum 
        (fac (- n 1) (* accum n)))

(defn factorial (n) (fac n 1))

Như bạn có thể thấy, đối số thứ hai để fac được sử dụng như một bộ tích lũy có thể thay đổi để chứa sản phẩm thay đổi nhanh n * (n-1) * (n-2) * .... Tuy nhiên, không có biến đổi có thể nhìn thấy được và không có cách nào để vô tình thay đổi bộ tích lũy, ví dụ từ một luồng khác.

Tất nhiên, đây là một ví dụ hạn chế.

Bạn có thể nhận được danh sách liên kết không thay đổi với nút thay thế giá rẻ (và bằng cách mở rộng bất kỳ phần nào bắt đầu từ đầu): bạn chỉ cần tạo điểm đầu mới đến cùng nút tiếp theo như đầu cũ đã làm. Điều này hoạt động tốt với nhiều thuật toán xử lý danh sách (bất cứ điều gìfold dựa trên ).

Bạn có thể có được hiệu suất khá tốt từ các mảng kết hợp dựa trên HAMTs . Theo logic, bạn nhận được một mảng kết hợp mới với một số cặp giá trị khóa được thay đổi. Việc thực hiện có thể chia sẻ hầu hết các dữ liệu phổ biến giữa các đối tượng cũ và mới được tạo. Đây không phải là O (1); thông thường bạn nhận được một cái gì đó logarit, ít nhất là trong trường hợp xấu nhất. Mặt khác, cây bất biến thường không phải chịu bất kỳ hình phạt hiệu suất nào so với cây đột biến. Tất nhiên, điều này đòi hỏi một số chi phí bộ nhớ, thường là xa cấm.

Một cách tiếp cận khác dựa trên ý tưởng rằng nếu một cái cây rơi trong rừng và không ai nghe thấy nó, thì nó không cần phải tạo ra âm thanh. Đó là, nếu bạn có thể chứng minh rằng một chút trạng thái đột biến không bao giờ rời khỏi một phạm vi cục bộ nào đó, bạn có thể biến đổi dữ liệu trong đó một cách an toàn.

Clojure có các quá độ có thể thay đổi 'bóng' của các cấu trúc dữ liệu bất biến không bị rò rỉ ra ngoài phạm vi cục bộ. Clean sử dụng Uniques để đạt được một cái gì đó tương tự (nếu tôi nhớ chính xác). Rust giúp làm những việc tương tự với con trỏ độc đáo được kiểm tra tĩnh.


1
+1, cũng để đề cập đến các loại duy nhất trong Clean.
Giorgio

@ 9000 Tôi nghĩ rằng tôi đã nghe nói rằng Haskell có một cái gì đó tương tự như tạm thời của Clojure - ai đó sửa tôi nếu tôi sai.
paul

@paul: Tôi có kiến ​​thức rất khó hiểu về Haskell, vì vậy nếu bạn có thể cung cấp thông tin của tôi (ít nhất là một từ khóa cho google), tôi rất vui lòng bao gồm một tham chiếu đến câu trả lời.
9000

1
@paul Tôi không chắc lắm. Nhưng Haskell có một phương pháp tạo ra thứ gì đó tương tự như của ML refvà ràng buộc chúng trong một phạm vi nhất định. Xem IORefhoặc STRef. Và dĩ nhiên, có những TVars và MVars tương tự nhau nhưng với ngữ nghĩa đồng thời lành mạnh (stm cho TVars và mutex dựa trên MVars)
Daniel Gratzer 23/214

2

Những gì bạn đang hỏi là một chút quá rộng. O (1) loại bỏ và thay thế từ vị trí nào? Người đứng đầu một chuỗi? Cái đuôi? Một vị trí độc đoán? Cấu trúc dữ liệu để sử dụng phụ thuộc vào những chi tiết đó. Điều đó nói rằng, 2-3 Cây ngón tay dường như là một trong những cấu trúc dữ liệu bền vững linh hoạt nhất hiện có:

Chúng tôi trình bày 2-3 cây ngón tay, một biểu diễn chức năng của các chuỗi liên tục hỗ trợ truy cập vào các đầu trong thời gian không đổi được khấu hao, và nối và tách trong logarit thời gian theo kích thước của mảnh nhỏ hơn.

(...)

Hơn nữa, bằng cách xác định thao tác phân tách ở dạng chung, chúng tôi có được cấu trúc dữ liệu mục đích chung có thể phục vụ như một chuỗi, hàng đợi ưu tiên, cây tìm kiếm, hàng đợi tìm kiếm ưu tiên và hơn thế nữa.

Nói chung các cấu trúc dữ liệu liên tục có hiệu suất logarit khi thay đổi các vị trí tùy ý. Điều này có thể hoặc không phải là một vấn đề, vì hằng số trong thuật toán O (1) có thể cao và sự chậm lại logarit có thể được "hấp thụ" vào một thuật toán tổng thể chậm hơn.

Quan trọng hơn, các cấu trúc dữ liệu liên tục giúp cho việc lập luận về chương trình của bạn dễ dàng hơn và đó luôn luôn là chế độ hoạt động mặc định của bạn. Bạn nên ưu tiên các cấu trúc dữ liệu liên tục bất cứ khi nào có thể và chỉ sử dụng cấu trúc dữ liệu có thể thay đổi một khi bạn đã định hình và xác định rằng cấu trúc dữ liệu liên tục là một nút cổ chai hiệu năng. Bất cứ điều gì khác là tối ưu hóa sớm.

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.