Trong lập trình chức năng, việc có hầu hết các cấu trúc dữ liệu bất biến đòi hỏi sử dụng nhiều bộ nhớ hơn?


63

Trong lập trình chức năng vì hầu như tất cả các cấu trúc dữ liệu là bất biến, khi trạng thái phải thay đổi một cấu trúc mới được tạo. Điều này có nghĩa là sử dụng nhiều bộ nhớ hơn? Tôi biết rõ mô hình lập trình hướng đối tượng, bây giờ tôi đang cố gắng tìm hiểu về mô hình lập trình chức năng. Khái niệm về tất cả mọi thứ là bất biến làm tôi bối rối. Có vẻ như một chương trình sử dụng các cấu trúc bất biến sẽ đòi hỏi nhiều bộ nhớ hơn so với một chương trình có cấu trúc có thể thay đổi. Tôi thậm chí đang nhìn điều này theo cách đúng đắn?


7
có thể có nghĩa là, nhưng hầu hết các cấu trúc dữ liệu bất biến sử dụng lại dữ liệu cơ bản cho các thay đổi. Eric Lippert có một loạt blog tuyệt vời về sự bất biến trong C #
Oded

3
Tôi sẽ xem qua Cấu trúc dữ liệu chức năng thuần túy, Đó là một cuốn sách tuyệt vời được viết bởi cùng một người đã viết hầu hết thư viện container của Haskell (mặc dù cuốn sách chủ yếu là SML)
jozefg

1
Câu trả lời này, liên quan đến thời gian chạy thay vì tiêu thụ bộ nhớ, cũng có thể thú vị đối với bạn: stackoverflow.com/questions/1990464/iêu
9000

1
Bạn có thể thấy điều này thú vị: vi.wikipedia.org/wiki/Static_single_ass
que_form

Câu trả lời:


35

Câu trả lời đúng duy nhất cho điều này là "đôi khi". Có rất nhiều thủ thuật mà các ngôn ngữ chức năng có thể sử dụng để tránh lãng phí bộ nhớ. Tính không thay đổi giúp chia sẻ dữ liệu giữa các hàm dễ dàng hơn và thậm chí giữa các cấu trúc dữ liệu, vì trình biên dịch có thể đảm bảo rằng dữ liệu sẽ không bị sửa đổi. Các ngôn ngữ chức năng có xu hướng khuyến khích việc sử dụng các cấu trúc dữ liệu có thể được sử dụng một cách hiệu quả như các cấu trúc bất biến (ví dụ, cây thay vì bảng băm). Nếu bạn thêm sự lười biếng vào hỗn hợp, giống như nhiều ngôn ngữ chức năng khác, điều đó sẽ thêm các cách mới để tiết kiệm bộ nhớ (nó cũng thêm các cách lãng phí bộ nhớ mới, nhưng tôi sẽ không đi sâu vào vấn đề đó).


24

Trong lập trình chức năng vì hầu như tất cả các cấu trúc dữ liệu là bất biến, khi trạng thái phải thay đổi một cấu trúc mới được tạo. Điều này có nghĩa là sử dụng nhiều bộ nhớ hơn?

Điều đó phụ thuộc vào cấu trúc dữ liệu, những thay đổi chính xác mà bạn đã thực hiện và, trong một số trường hợp, trình tối ưu hóa. Như một ví dụ, hãy xem xét việc chuẩn bị trước một danh sách:

list2 = prepend(42, list1) // list2 is now a list that contains 42 followed
                           // by the elements of list1. list1 is unchanged

Ở đây, yêu cầu bộ nhớ bổ sung là không đổi - chi phí thời gian gọi của cuộc gọi cũng vậy prepend. Tại sao? Bởi vì prependđơn giản là tạo ra một tế bào mới có 42đầu và list1đuôi. Nó không phải sao chép hoặc lặp đi lặp lại list2để đạt được điều này. Đó là, ngoại trừ bộ nhớ cần thiết để lưu trữ 42, list2sử dụng lại cùng bộ nhớ được sử dụng list1. Vì cả hai danh sách là bất biến, chia sẻ này là hoàn toàn an toàn.

Tương tự, khi làm việc với các cấu trúc cây cân bằng, hầu hết các hoạt động chỉ cần một lượng logarit của không gian bổ sung vì mọi thứ, nhưng một đường dẫn của cây có thể được chia sẻ.

Đối với mảng tình huống là một chút khác nhau. Đó là lý do tại sao, trong nhiều ngôn ngữ FP, mảng không được sử dụng phổ biến. Tuy nhiên, nếu bạn làm một cái gì đó giống arr2 = map(f, arr1)arr1không bao giờ được sử dụng lại sau dòng này, trình tối ưu hóa thông minh thực sự có thể tạo mã arr1thay đổi thay vì tạo một mảng mới (mà không ảnh hưởng đến hành vi của chương trình). Trong trường hợp đó, hiệu suất sẽ như trong một ngôn ngữ bắt buộc tất nhiên.


1
Không quan tâm, việc thực hiện ngôn ngữ nào sẽ sử dụng lại không gian như bạn đã mô tả gần cuối?

@delnan Tại trường đại học của tôi có một ngôn ngữ nghiên cứu tên là Qube, đã làm điều đó. Mặc dù vậy, tôi không biết liệu có bất kỳ ngôn ngữ được sử dụng trong tự nhiên nào thực hiện điều này hay không. Tuy nhiên, phản ứng tổng hợp của Haskell có thể đạt được hiệu quả tương tự trong nhiều trường hợp.
sepp2k

7

Việc triển khai ngây thơ thực sự sẽ phơi bày vấn đề này - khi bạn tạo một cấu trúc dữ liệu mới thay vì cập nhật một cấu trúc hiện có tại chỗ, bạn phải có một số chi phí.

Các ngôn ngữ khác nhau có cách xử lý khác nhau và có một vài thủ thuật mà hầu hết chúng sử dụng.

Một chiến lược là thu gom rác . Thời điểm cấu trúc mới đã được tạo, hoặc ngay sau đó, các tham chiếu đến cấu trúc cũ sẽ vượt ra ngoài phạm vi và trình thu gom rác sẽ chọn nó ngay lập tức hoặc sớm, tùy thuộc vào thuật toán GC. Điều này có nghĩa là trong khi vẫn còn chi phí hoạt động, nó chỉ là tạm thời và sẽ không tăng trưởng tuyến tính với lượng dữ liệu.

Một số khác là chọn các loại cấu trúc dữ liệu khác nhau. Trong đó các mảng là cấu trúc dữ liệu đi đến danh sách các ngôn ngữ bắt buộc (thường được gói trong một số loại thùng chứa phân bổ lại động như std::vectortrong C ++), các ngôn ngữ chức năng thường thích các danh sách được liên kết. Với danh sách được liên kết, thao tác trả trước ('nhược điểm') có thể sử dụng lại danh sách hiện tại làm đuôi của danh sách mới, vì vậy tất cả những gì thực sự được phân bổ là đầu danh sách mới. Các chiến lược tương tự tồn tại cho các loại cấu trúc dữ liệu khác - bộ, cây, bạn đặt tên cho nó.

Và sau đó là đánh giá lười biếng, à la Haskell. Ý tưởng là các cấu trúc dữ liệu bạn tạo không được tạo hoàn toàn ngay lập tức; thay vào đó, chúng được lưu trữ dưới dạng "thunks" (bạn có thể coi đây là những công thức để xây dựng giá trị khi cần thiết). Chỉ khi giá trị là cần thiết, thunk mới được mở rộng thành giá trị thực. Điều này có nghĩa là việc cấp phát bộ nhớ có thể được hoãn lại cho đến khi cần đánh giá và tại thời điểm đó, một số thunks có thể được kết hợp trong một cấp phát bộ nhớ.


Wow, một câu trả lời nhỏ và rất nhiều thông tin / cái nhìn sâu sắc. Cảm ơn bạn :)
Gerry

3

Tôi chỉ biết một chút về Clojure và đó là Cấu trúc dữ liệu bất biến .

Clojure cung cấp một tập hợp các danh sách, vectơ, bộ và bản đồ bất biến. Vì chúng không thể được thay đổi, 'thêm' hoặc 'loại bỏ' thứ gì đó khỏi bộ sưu tập bất biến có nghĩa là tạo ra một bộ sưu tập mới giống như bộ sưu tập cũ nhưng với sự thay đổi cần thiết. Sự kiên trì là một thuật ngữ được sử dụng để mô tả thuộc tính trong đó phiên bản cũ của bộ sưu tập vẫn có sẵn sau khi 'thay đổi' và bộ sưu tập duy trì đảm bảo hiệu suất của nó cho hầu hết các hoạt động. Cụ thể, điều này có nghĩa là phiên bản mới không thể được tạo bằng bản sao đầy đủ, vì điều đó sẽ yêu cầu thời gian tuyến tính. Chắc chắn, các bộ sưu tập liên tục được triển khai bằng các cấu trúc dữ liệu được liên kết, để các phiên bản mới có thể chia sẻ cấu trúc với phiên bản trước.

Về mặt đồ họa, chúng ta có thể đại diện cho một cái gì đó như thế này:

(def my-list '(1 2 3))

    +---+      +---+      +---+
    | 1 | ---> | 2 | ---> | 3 |
    +---+      +---+      +---+

(def new-list (conj my-list 0))

              +-----------------------------+
    +---+     | +---+      +---+      +---+ |
    | 0 | --->| | 1 | ---> | 2 | ---> | 3 | |
    +---+     | +---+      +---+      +---+ |
              +-----------------------------+

2

Ngoài những gì đã được nói trong các câu trả lời khác, tôi muốn đề cập đến ngôn ngữ lập trình Clean, hỗ trợ cái gọi là các loại duy nhất . Tôi không biết ngôn ngữ này nhưng tôi cho rằng các loại duy nhất hỗ trợ một số loại "cập nhật phá hoại".

Nói cách khác, trong khi ngữ nghĩa của việc cập nhật trạng thái là bạn tạo một giá trị mới từ một trạng thái cũ bằng cách áp dụng một hàm, thì ràng buộc duy nhất có thể cho phép trình biên dịch sử dụng lại các đối tượng dữ liệu trong nội bộ vì nó biết rằng giá trị cũ sẽ không được tham chiếu bất kỳ nữa trong chương trình sau khi giá trị mới đã được sản xuất.

Để biết thêm chi tiết, xem ví dụ trang chủ Cleanbài viết trên wikipedia này

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.