Là một chức năng thuần túy ghi nhớ được coi là tinh khiết?


47

Giả sử fn(x)là một hàm thuần túy thực hiện một cái gì đó đắt tiền, như trả về một danh sách các yếu tố chính x.

Và giả sử chúng ta tạo một phiên bản ghi nhớ của cùng chức năng được gọi memoizedFn(x). Nó luôn trả về cùng một kết quả cho một đầu vào nhất định, nhưng nó duy trì bộ đệm riêng của các kết quả trước đó để cải thiện hiệu suất.

Chính thức nói, được memoizedFn(x)coi là thuần túy?

Hoặc có một số tên hoặc thuật ngữ đủ điều kiện khác được sử dụng để đề cập đến một chức năng như vậy trong các cuộc thảo luận về FP? (tức là một hàm có tác dụng phụ có thể ảnh hưởng đến độ phức tạp tính toán của các cuộc gọi tiếp theo nhưng điều đó có thể không ảnh hưởng đến các giá trị trả về.)


24
Có thể nó không thuần túy đối với những người theo chủ nghĩa thuần túy, nhưng "đủ tinh khiết" cho những người thực dụng ;-)
Doc Brown

2
@DocBrown Tôi đồng ý, chỉ tự hỏi liệu có một thuật ngữ chính thức hơn cho 'đủ tinh khiết'
callum

13
Thực hiện một chức năng thuần túy rất có thể sẽ sửa đổi bộ đệm hướng dẫn của bộ xử lý, bộ dự đoán nhánh, v.v.
gnasher729

10
@callum Không, không có định nghĩa chính thức về "đủ tinh khiết". Khi tranh luận về độ tinh khiết và sự tương đương về ngữ nghĩa của hai cuộc gọi "minh bạch tham chiếu", bạn luôn phải nêu chính xác ngữ nghĩa mà bạn sẽ áp dụng. Ở một số mức độ chi tiết thực hiện thấp, nó sẽ luôn bị hỏng và có các hiệu ứng hoặc thời gian bộ nhớ khác nhau. Đó là lý do tại sao bạn phải thực dụng: mức độ chi tiết nào hữu ích cho lý luận về mã của bạn?
Bergi

3
Sau đó, vì lợi ích của chủ nghĩa thực dụng, tôi muốn nói rằng độ tinh khiết phụ thuộc vào việc bạn có coi thời gian tính toán là một phần của đầu ra hay không. funcx(){sleep(cached_time--); return 0;}trả về cùng một giá trị mỗi lần, nhưng sẽ hoạt động khác nhau
Sao Hỏa

Câu trả lời:


41

Đúng. Phiên bản ghi nhớ của một hàm thuần túy cũng là một hàm thuần túy.

Tất cả sự tinh khiết của chức năng quan tâm là hiệu ứng mà các tham số đầu vào trên giá trị trả về của hàm (truyền cùng một đầu vào phải luôn tạo ra cùng một đầu ra) và bất kỳ tác dụng phụ nào liên quan đến trạng thái toàn cầu (ví dụ: văn bản đến thiết bị đầu cuối hoặc giao diện người dùng hoặc mạng) . Thời gian tính toán và sử dụng bộ nhớ thêm không liên quan đến độ tinh khiết của chức năng.

Bộ nhớ cache của một chức năng thuần túy là khá nhiều vô hình cho chương trình; một ngôn ngữ lập trình chức năng được phép tự động tối ưu hóa một chức năng thuần túy thành phiên bản ghi nhớ của chức năng nếu nó có thể xác định rằng nó sẽ có ích khi làm như vậy. Trong thực tế, việc tự động xác định khi ghi nhớ có lợi thực sự là một vấn đề khá khó khăn, nhưng tối ưu hóa như vậy sẽ có hiệu lực.


19

Wikipedia định nghĩa "Hàm thuần túy" là một hàm có các thuộc tính sau:

  • Giá trị trả về của nó là giống nhau cho cùng một đối số (không có biến thể với biến tĩnh cục bộ, biến không cục bộ, đối số tham chiếu có thể thay đổi hoặc luồng đầu vào từ thiết bị I / O).

  • Đánh giá của nó không có tác dụng phụ (không có đột biến biến tĩnh cục bộ, biến không cục bộ, đối số tham chiếu có thể thay đổi hoặc luồng I / O).

Trong thực tế, một hàm thuần trả về cùng một đầu ra cho cùng một đầu vào và không ảnh hưởng đến bất kỳ thứ gì khác ngoài hàm. Đối với mục đích của độ tinh khiết, việc hàm tính toán giá trị trả về của nó không quan trọng, miễn là nó trả về cùng một đầu ra cho cùng một đầu vào.

Các ngôn ngữ thuần túy chức năng như Haskell thường xuyên sử dụng ghi nhớ để tăng tốc chức năng bằng cách lưu trữ các kết quả được tính toán trước đó.


16
Tôi có thể bỏ lỡ điều gì đó, nhưng làm thế nào để bạn giữ bộ nhớ cache mà không có tác dụng phụ?
val

1
Bằng cách giữ nó bên trong chức năng.
Robert Harvey

4
"Không có đột biến của biến tĩnh cục bộ" dường như cũng loại trừ các biến cục bộ liên tục giữa các cuộc gọi.
val

3
Điều này không thực sự trả lời câu hỏi, ngay cả khi bạn dường như ngụ ý rằng có, nó là thuần túy.
Sao Hỏa

6
@val Bạn đã đúng: điều kiện này cần được thư giãn một chút. Việc ghi nhớ hoàn toàn chức năng mà ông đề cập đến không đột biến rõ ràng của bất kỳ dữ liệu tĩnh nào. Điều xảy ra là kết quả sau đó được tính toán và ghi nhớ lần đầu tiên hàm được gọi và trả về cùng một giá trị bất cứ khi nào nó được gọi. Nhiều ngôn ngữ có một thành ngữ cho điều đó: một static constbiến cục bộ trong C ++ (nhưng không phải C) hoặc cấu trúc dữ liệu được đánh giá lười biếng trong Haskell. Có một điều kiện nữa bạn cần: việc khởi tạo phải an toàn cho luồng.
Davislor

7

Có, các hàm thuần túy được ghi nhớ thường được gọi là thuần. Điều này đặc biệt phổ biến trong các ngôn ngữ như Haskell, trong đó các kết quả được ghi nhớ, đánh giá lười biếng, không thay đổi là một tính năng tích hợp.

Có một cảnh báo quan trọng: chức năng ghi nhớ phải an toàn cho luồng hoặc nếu không bạn có thể gặp tình trạng chạy đua khi cả hai luồng đều cố gắng gọi nó.

Một ví dụ về một nhà khoa học máy tính sử dụng thuật ngữ hoàn toàn có chức năng, theo cách này là bài đăng trên blog của Conal Elliott về ghi nhớ tự động:

Có lẽ đáng ngạc nhiên, ghi nhớ có thể được thực hiện đơn giản và hoàn toàn chức năng trong một ngôn ngữ chức năng lười biếng.

Có nhiều ví dụ trong các tài liệu đánh giá ngang hàng, và đã được nhiều thập kỷ. Ví dụ, bài báo này từ năm 1995, sử dụng Ghi nhớ tự động như một công cụ kỹ thuật phần mềm trong các hệ thống AI trong thế giới thực, để sử dụng ngôn ngữ rất giống nhau trong phần 5.2 để mô tả cái mà ngày nay chúng ta gọi là hàm thuần túy:

Ghi nhớ chỉ hoạt động cho các chức năng thực sự, không phải thủ tục. Đó là, nếu kết quả của hàm không hoàn toàn và được chỉ định một cách xác định bởi các tham số đầu vào của nó, sử dụng ghi nhớ sẽ cho kết quả không chính xác. Số lượng các chức năng có thể được ghi nhớ thành công sẽ được tăng lên bằng cách khuyến khích sử dụng một kiểu lập trình chức năng trong toàn hệ thống.

Một số ngôn ngữ mệnh lệnh có một thành ngữ tương tự. Ví dụ, một static constbiến trong C ++ chỉ được khởi tạo một lần, trước khi giá trị của nó được sử dụng và không bao giờ biến đổi.


3

Nó phụ thuộc vào cách bạn làm điều đó.

Thông thường mọi người muốn ghi nhớ bằng cách thay đổi một số loại từ điển bộ đệm. Điều này có tất cả các vấn đề liên quan đến đột biến không tinh khiết, chẳng hạn như phải lo lắng về sự tương tranh, lo lắng về việc bộ đệm phát triển quá lớn, v.v.

Tuy nhiên, bạn có thể ghi nhớ mà không bị đột biến bộ nhớ. Một ví dụ là trong câu trả lời này , nơi tôi theo dõi các giá trị được ghi nhớ bên ngoài bằng phương tiện của một lengthsđối số.

Trong liên kết Robert Harvey cung cấp , đánh giá lười biếng được sử dụng để tránh tác dụng phụ.

Một kỹ thuật khác đôi khi được nhìn thấy là đánh dấu rõ ràng việc ghi nhớ là một hiệu ứng phụ không tinh khiết trong ngữ cảnh của một IOloại, chẳng hạn như với chức năng ghi nhớ của hiệu ứng mèo .

Điều cuối cùng này đưa ra quan điểm rằng đôi khi mục tiêu chỉ là gói gọn sự đột biến hơn là loại bỏ nó. Hầu hết các lập trình viên chức năng coi nó "đủ tinh khiết" để làm cho tạp chất rõ ràng và được gói gọn.

Nếu bạn muốn một thuật ngữ để phân biệt nó với một chức năng thực sự thuần túy, tôi nghĩ chỉ cần nói "ghi nhớ với một từ điển có thể thay đổi" là đủ. Điều đó cho phép mọi người biết làm thế nào để sử dụng nó một cách an toàn.


Tôi không nghĩ rằng bất kỳ giải pháp thuần túy nào cũng giải quyết được các vấn đề trên: Trong khi bạn mất bất kỳ lo lắng đồng thời nào, bạn cũng sẽ mất bất kỳ cơ hội nào cho hai cuộc gọi bắt đầu đồng thời như collatz(100)collatz(200)hợp tác. Và IIUIC, vấn đề với bộ đệm phát triển quá lớn vẫn còn (mặc dù Haskell có thể có một số thủ thuật hay cho việc này?).
maaartinus

Lưu ý: IOlà tinh khiết. Tất cả các phương pháp không tinh khiết trên IOvà Mèo được đặt tên unsafe. Async.memoizecũng thuần khiết, vì vậy chúng tôi không phải giải quyết "đủ tinh khiết" :)
Samuel

2

Thông thường, một hàm trả về một danh sách hoàn toàn không thuần túy bởi vì nó yêu cầu phân bổ dung lượng lưu trữ và do đó có thể thất bại (ví dụ: bằng cách ném một ngoại lệ, không thuần túy). Một ngôn ngữ có các loại giá trị và có thể biểu thị một danh sách dưới dạng loại giá trị kích thước giới hạn có thể không có vấn đề này. Vì lý do này, ví dụ của bạn có thể không thuần túy.

Nói chung, nếu việc ghi nhớ có thể được thực hiện theo cách không xảy ra trường hợp không có lỗi (ví dụ: bằng cách lưu trữ được phân bổ tĩnh cho kết quả ghi nhớ và đồng bộ hóa nội bộ để kiểm soát quyền truy cập vào chúng nếu ngôn ngữ chấp nhận các luồng), việc xem xét chức năng đó là hợp lý nguyên chất.


0

Bạn có thể thực hiện ghi nhớ mà không có tác dụng phụ bằng cách sử dụng đơn nguyên trạng thái .

[Trạng thái đơn vị] về cơ bản là một hàm S => (S, A), trong đó S là loại đại diện cho trạng thái của bạn và A là kết quả mà hàm tạo ra - Trạng thái Mèo .

Trong trường hợp của bạn, trạng thái sẽ là giá trị ghi nhớ hoặc không có gì (ví dụ: Haskell Maybehoặc Scala Option[A]). Nếu có giá trị ghi nhớ, nó được trả về dưới dạng Akhác, Ađược tính và trả về cả trạng thái được chuyển đổi và kết quả.

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.