Tại sao khái niệm đánh giá lười biếng hữu ích?


30

Dường như việc đánh giá các biểu thức lười biếng có thể khiến một lập trình viên mất kiểm soát đối với thứ tự mà mã của họ được thực thi. Tôi gặp khó khăn trong việc hiểu tại sao điều này sẽ được chấp nhận hoặc mong muốn bởi một lập trình viên.

Làm thế nào mô hình này có thể được sử dụng để xây dựng phần mềm dự đoán hoạt động như dự định, khi chúng tôi không đảm bảo khi nào và ở đâu một biểu thức sẽ được đánh giá?


10
Trong hầu hết các trường hợp, điều đó không thành vấn đề. Đối với tất cả những người khác, bạn chỉ có thể thực thi nghiêm ngặt.
Cat Plus Plus

22
Điểm quan trọng của các ngôn ngữ chức năng thuần túy như haskell là bạn không phải bận tâm khi mã được chạy, vì nó không có tác dụng phụ.
bitmask

21
Bạn cần ngừng suy nghĩ về "thực thi mã" và bắt đầu nghĩ đến "tính kết quả", vì đó là điều bạn thực sự muốn trong hầu hết các vấn đề thú vị. Tất nhiên các chương trình thường cũng cần phải tương tác với môi trường theo một cách nào đó, nhưng điều đó thường có thể được giảm xuống một phần nhỏ của mã. Đối với phần còn lại, bạn có thể làm việc hoàn toàn chức năng , và ở đó sự lười biếng có thể làm cho lý luận đơn giản hơn rất nhiều.
leftaroundabout

6
Câu hỏi trong tiêu đề ("Tại sao sử dụng đánh giá lười biếng?") Rất khác với câu hỏi trong cơ thể ("Làm thế nào để bạn sử dụng đánh giá lười biếng?"). Đối với trước đây, xem câu trả lời của tôi cho câu hỏi liên quan này .
Daniel Wagner

1
Một ví dụ khi sự lười biếng là hữu ích: Trong Haskell head . sortO(n)sự phức tạp do sự lười biếng (không O(n log n)). Xem Đánh giá lười biếng và độ phức tạp thời gian .
Petr Pudlák

Câu trả lời:


62

Rất nhiều câu trả lời đang đi vào những thứ như danh sách vô hạn và hiệu suất đạt được từ các phần không được đánh giá của tính toán, nhưng điều này thiếu động lực lớn hơn cho sự lười biếng: tính mô đun .

Lập luận cổ điển được trình bày trong bài báo được trích dẫn nhiều nhất "Tại sao các vấn đề lập trình chức năng" (liên kết PDF) của John Hughes. Ví dụ chính trong bài báo đó (Phần 5) đang chơi Tic-Tac-Toe bằng thuật toán tìm kiếm alpha-beta. Điểm chính là (trang 9):

[Đánh giá lười biếng] làm cho nó thực tế khi mô đun hóa một chương trình như một trình tạo xây dựng một số lượng lớn các câu trả lời có thể, và một bộ chọn chọn một câu trả lời thích hợp.

Chương trình Tic-Tac-Toe có thể được viết dưới dạng một hàm tạo ra toàn bộ cây trò chơi bắt đầu từ một vị trí nhất định và một hàm riêng biệt tiêu thụ nó. Trong thời gian chạy, điều này thực chất không tạo ra toàn bộ cây trò chơi, chỉ có những nhánh con mà người tiêu dùng thực sự cần. Chúng ta có thể thay đổi thứ tự và sự kết hợp trong đó các lựa chọn thay thế được sản xuất bằng cách thay đổi người tiêu dùng; không cần thay đổi máy phát điện cả.

Trong một ngôn ngữ háo hức, bạn không thể viết nó theo cách này bởi vì bạn có thể sẽ dành quá nhiều thời gian và bộ nhớ để tạo ra cây. Vì vậy, bạn kết thúc một trong hai:

  1. Kết hợp việc tạo và tiêu thụ vào cùng một chức năng;
  2. Viết một nhà sản xuất làm việc tối ưu chỉ dành cho người tiêu dùng nhất định;
  3. Thực hiện phiên bản lười biếng của riêng bạn.

Xin vui lòng thêm thông tin hoặc một ví dụ. Điều này nghe có vẻ hấp dẫn.
Alex Nye

1
@AlexNye: Bài báo của John Hughes có nhiều thông tin hơn. Mặc dù là một bài báo học thuật --- và do đó không còn nghi ngờ gì nữa --- nó thực sự rất dễ tiếp cận và dễ đọc. Nếu không phải là chiều dài của nó, nó có lẽ sẽ phù hợp như một câu trả lời ở đây!
Tikhon Jelvis

Có lẽ để hiểu câu trả lời này, người ta phải đọc bài báo của Hughes ... không làm như vậy, tôi vẫn không biết làm thế nào và tại sao sự lười biếng và tính mô đun có liên quan.
stakx

@stakx Nếu không có mô tả tốt hơn, dường như chúng không liên quan ngoại trừ tình cờ. Ưu điểm của sự lười biếng trong ví dụ này là một trình tạo lười biếng có khả năng tạo ra tất cả các trạng thái có thể có của trò chơi, nhưng sẽ không lãng phí thời gian / bộ nhớ khi làm như vậy bởi vì chỉ những thứ xảy ra sẽ được tiêu thụ. Máy phát điện có thể được tách ra khỏi người tiêu dùng mà không phải là máy phát lười biếng, và có thể (mặc dù khó khăn hơn) là lười biếng mà không bị tách khỏi người tiêu dùng.
Izkata

@Iztaka: "Máy phát điện có thể được tách ra khỏi người tiêu dùng mà không phải là máy phát lười biếng, và có thể (mặc dù khó khăn hơn) để lười biếng mà không bị tách khỏi người tiêu dùng." Lưu ý rằng khi bạn đi theo tuyến đường này, bạn có thể kết thúc với ** trình tạo chuyên dụng quá mức ** - trình tạo được viết để tối ưu hóa một người tiêu dùng và khi được sử dụng lại cho những người khác thì nó không tối ưu. Một ví dụ phổ biến là các trình ánh xạ quan hệ đối tượng tìm nạp và xây dựng toàn bộ biểu đồ đối tượng chỉ vì bạn muốn một chuỗi từ đối tượng gốc. Sự lười biếng tránh được nhiều trường hợp như vậy (nhưng không phải tất cả).
sacundim

32

Làm thế nào mô hình này có thể được sử dụng để xây dựng phần mềm dự đoán hoạt động như dự định, khi chúng tôi không đảm bảo khi nào và ở đâu một biểu thức sẽ được đánh giá?

Khi một biểu thức không có tác dụng phụ, thứ tự các biểu thức được ước tính không ảnh hưởng đến giá trị của chúng, vì vậy hành vi của chương trình không bị ảnh hưởng bởi thứ tự. Vì vậy, hành vi là hoàn toàn có thể dự đoán.

Bây giờ tác dụng phụ là một vấn đề khác nhau. Nếu các tác dụng phụ có thể xảy ra theo bất kỳ thứ tự nào, hành vi của chương trình thực sự sẽ không thể đoán trước được. Nhưng đây không thực sự là trường hợp. Các ngôn ngữ lười biếng như Haskell làm cho nó trở thành một điểm minh bạch về mặt tham chiếu, tức là đảm bảo rằng thứ tự các biểu thức được đánh giá sẽ không bao giờ ảnh hưởng đến kết quả của chúng. Trong Haskell, điều này đạt được bằng cách buộc tất cả các hoạt động với các tác dụng phụ có thể nhìn thấy của người dùng xảy ra bên trong đơn vị IO. Điều này đảm bảo rằng tất cả các tác dụng phụ xảy ra chính xác theo thứ tự bạn mong đợi.


15
Đây là lý do tại sao chỉ các ngôn ngữ có "độ tinh khiết bắt buộc" như Haskell hỗ trợ sự lười biếng ở mọi nơi theo mặc định. Các ngôn ngữ "Độ tinh khiết được khuyến khích" như Scala cần lập trình viên nói rõ ràng nơi họ muốn lười biếng, và điều đó tùy thuộc vào lập trình viên để đảm bảo rằng sự lười biếng sẽ không gây ra vấn đề. Một ngôn ngữ có sự lười biếng theo mặc định có tác dụng phụ không được theo dõi thực sự sẽ rất khó để lập trình dự đoán.
Ben

1
chắc chắn các đơn vị khác ngoài IO cũng có thể gây ra tác dụng phụ
jk.

1
@jk Chỉ IO có thể gây ra tác dụng phụ bên ngoài .
dave4420

@ dave4420 có nhưng đó không phải là câu trả lời này
jk.

1
@jk Trong Haskell thực sự không có. Không có đơn vị nào ngoại trừ IO (hoặc những người xây dựng trên IO) có tác dụng phụ. Và điều này chỉ bởi vì trình biên dịch xử lý IO khác nhau. Nó nghĩ về IO là "Tắt bất biến". Monads chỉ là một cách (thông minh) để đảm bảo thứ tự thực hiện cụ thể (vì vậy tệp của bạn sẽ chỉ bị xóa sau khi người dùng nhập "có").
Scarfridge

22

Nếu bạn quen thuộc với cơ sở dữ liệu, một cách rất thường xuyên để xử lý dữ liệu là:

  • Đặt một câu hỏi như select * from foobar
  • Trong khi có nhiều dữ liệu hơn, hãy thực hiện: lấy hàng kết quả tiếp theo và xử lý nó

Bạn không kiểm soát cách tạo kết quả và theo cách nào (chỉ mục? Quét toàn bộ bảng?) Hoặc khi nào (tất cả dữ liệu sẽ được tạo cùng một lúc hay tăng dần khi được yêu cầu?). Tất cả những gì bạn biết là: nếu có nhiều dữ liệu hơn, bạn sẽ nhận được nó khi bạn yêu cầu.

Đánh giá lười biếng là khá gần với điều tương tự. Giả sử bạn có một danh sách vô hạn được định nghĩa là nghĩa. dãy số Fibonacci - nếu bạn cần năm số, bạn sẽ có năm số được tính; nếu bạn cần 1000 bạn nhận được 1000. Bí quyết là thời gian chạy biết phải cung cấp những gì ở đâu và khi nào. Nó rất, rất tiện dụng.

(Các lập trình viên Java có thể mô phỏng hành vi này với các Trình lặp - các ngôn ngữ khác có thể có nội dung tương tự)


Điểm tốt. Ví dụ Collection2.filter()(cũng như các phương thức khác từ lớp đó) thực hiện khá nhiều đánh giá lười biếng: kết quả "trông giống" một cách thông thường Collection, nhưng thứ tự thực hiện có thể không trực quan (hoặc ít nhất là không rõ ràng). Ngoài ra, có yieldtrong Python (và một tính năng tương tự trong C #, mà tôi không nhớ tên của nó) thậm chí còn gần với việc hỗ trợ đánh giá lười biếng hơn so với Iterator bình thường.
Joachim Sauer

@JoachimSauer trong C # lợi nhuận của nó, hoặc tất nhiên bạn có thể sử dụng các oprerators linq prebuild, khoảng một nửa trong số đó là lười biếng
jk.

+1: Để đề cập đến các trình vòng lặp trong ngôn ngữ bắt buộc / hướng đối tượng. Tôi đã sử dụng một giải pháp tương tự để thực hiện các luồng và các hàm truyền phát trong Java. Sử dụng các trình vòng lặp tôi có thể có các hàm như Take (n), dropWhile () trên luồng đầu vào có độ dài không xác định.
Giorgio

13

Cân nhắc hỏi cơ sở dữ liệu của bạn để biết danh sách 2000 người dùng đầu tiên có tên bắt đầu bằng "Ab" và cũ hơn 20 năm. Ngoài ra họ phải là nam giới.

Đây là một sơ đồ nhỏ.

You                                            Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
                         --------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
                         --------------------- NOW I'll get them...
WAIT! Make sure they're  ---------->---------- Good idea Boss!
over 20!
                         --------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!

No that is all. :(       ---------->---------- FINE! Getting records!

                         --------------------- Here you go. 
Thanks Postgres, you're  ---------->----------  ...
my only friend.

Như bạn có thể thấy bởi sự tương tác khủng khiếp khủng khiếp này, "cơ sở dữ liệu" không thực sự làm gì cho đến khi nó sẵn sàng xử lý tất cả các điều kiện. Đó là kết quả tải lười biếng ở mỗi bước và áp dụng điều kiện mới mỗi lần.

Trái ngược với việc có 2000 người dùng đầu tiên, trả lại họ, lọc họ cho "Ab", trả lại cho họ, lọc hơn 20, trả lại cho họ và lọc cho nam và cuối cùng trả lại cho họ.

Lười tải trong một nutshell.


1
Đây là một lời giải thích thực sự tệ hại IMHO. Thật không may, tôi không có đủ đại diện trên trang SE cụ thể này để bỏ phiếu. Điểm thực sự của đánh giá lười biếng là không có kết quả nào trong số này thực sự được tạo ra cho đến khi một thứ khác sẵn sàng để tiêu thụ chúng.
Alnitak

Câu trả lời được đăng của tôi là nói chính xác như nhận xét của bạn.
serserg

Đó là một Bộ xử lý chương trình rất lịch sự.
Julian

9

Đánh giá lười biếng các biểu thức sẽ khiến người thiết kế một đoạn mã nhất định mất quyền kiểm soát đối với chuỗi mã mà mã của họ được thực thi.

Nhà thiết kế không nên quan tâm đến thứ tự các biểu thức được đánh giá với điều kiện kết quả là như nhau. Bằng cách trì hoãn đánh giá, có thể tránh đánh giá một số biểu thức hoàn toàn, giúp tiết kiệm thời gian.

Bạn có thể thấy ý tưởng tương tự tại nơi làm việc ở mức thấp hơn: nhiều bộ vi xử lý có thể thực hiện các lệnh không theo thứ tự, cho phép chúng sử dụng các đơn vị thực thi khác nhau hiệu quả hơn. Điều quan trọng là họ xem xét sự phụ thuộc giữa các hướng dẫn và tránh sắp xếp lại nơi nó sẽ thay đổi kết quả.


5

Có một số lập luận cho đánh giá lười biếng tôi nghĩ là hấp dẫn

  1. Tính mô đun Với đánh giá lười biếng, bạn có thể chia mã thành nhiều phần. Ví dụ: giả sử bạn có vấn đề "tìm mười đối ứng đầu tiên của các phần tử trong danh sách danh sách sao cho các đối ứng nhỏ hơn 1." Trong một cái gì đó như Haskell bạn có thể viết

    take 10 . filter (<1) . map (1/)
    

    nhưng điều này không chính xác trong một ngôn ngữ nghiêm ngặt, vì nếu bạn cho nó, [2,3,4,5,6,7,8,9,10,11,12,0]bạn sẽ chia cho số không. Xem câu trả lời của sacundim cho lý do tại sao điều này là tuyệt vời trong thực tế

  2. Nhiều thứ hoạt động Nghiêm ngặt (ý định chơi chữ) nhiều chương trình chấm dứt với đánh giá không nghiêm ngặt hơn so với đánh giá nghiêm ngặt. Nếu chương trình của bạn chấm dứt với chiến lược đánh giá "háo hức", nó sẽ chấm dứt với chiến lược "lười biếng", nhưng điều ngược lại là không đúng. Bạn nhận được những thứ như cấu trúc dữ liệu vô hạn (thực sự chỉ tuyệt vời) như những ví dụ cụ thể của hiện tượng này. Nhiều chương trình hoạt động trong các ngôn ngữ lười biếng.

  3. Tối ưu Đánh giá cuộc gọi theo nhu cầu là tối ưu không có triệu chứng theo thời gian. Mặc dù các ngôn ngữ lười biếng chính (về cơ bản là Haskell và Haskell) không hứa hẹn về nhu cầu gọi điện, bạn ít nhiều có thể mong đợi một mô hình chi phí tối ưu. Máy phân tích nghiêm ngặt (và đánh giá đầu cơ) giữ cho chi phí hoạt động thấp trong thực tế. Không gian là một vấn đề phức tạp hơn.

  4. Buộc Lực lượng tinh khiết sử dụng đánh giá lười biếng làm cho việc xử lý các tác dụng phụ theo cách vô kỷ luật trở thành một nỗi đau hoàn toàn, bởi vì khi bạn đặt nó, lập trình viên mất kiểm soát. Đây là một điều tốt. Tính minh bạch tham chiếu làm cho lập trình, khúc xạ và lý luận về các chương trình trở nên dễ dàng hơn nhiều. Các ngôn ngữ nghiêm ngặt chắc chắn sẽ gây áp lực khi có các bit không tinh khiết - thứ gì đó mà Haskell và Clean đã chống lại rất đẹp. Điều này không có nghĩa là tác dụng phụ luôn xấu, nhưng kiểm soát chúng rất hữu ích đến nỗi chỉ riêng lý do này là đủ để sử dụng ngôn ngữ lười biếng.


2

Giả sử bạn có rất nhiều tính toán đắt tiền được cung cấp, nhưng không biết cái nào sẽ thực sự cần thiết, hoặc theo thứ tự nào. Bạn có thể thêm một giao thức mẹ-i-i phức tạp để buộc người tiêu dùng tìm ra những gì có sẵn và kích hoạt các tính toán chưa được thực hiện. Hoặc bạn chỉ có thể cung cấp một giao diện hoạt động như thể các phép tính đã hoàn tất.

Ngoài ra, giả sử bạn có một kết quả vô hạn. Tập hợp tất cả các số nguyên tố chẳng hạn. Rõ ràng là bạn không thể tính toán trước tập hợp, vì vậy mọi thao tác trong miền số nguyên tố phải được lười biếng.


1

với đánh giá lười biếng, bạn không mất quyền kiểm soát việc thực thi mã, nó vẫn hoàn toàn mang tính quyết định. Thật khó để làm quen với nó, mặc dù.

đánh giá lười biếng là hữu ích bởi vì đó là một cách giảm thời hạn lambda sẽ chấm dứt trong một số trường hợp, trong đó đánh giá háo hức sẽ thất bại, nhưng không phải là ngược lại. Điều này bao gồm 1) khi bạn cần liên kết với kết quả tính toán trước khi bạn thực sự thực hiện tính toán, ví dụ: khi bạn xây dựng cấu trúc biểu đồ tuần hoàn, nhưng muốn thực hiện theo kiểu chức năng 2) khi bạn xác định cấu trúc dữ liệu vô hạn, nhưng chức năng cung cấp cấu trúc này chỉ sử dụng một phần của cơ sở hạ tầng.

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.