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


119

Tôi từ lâu đã tự hỏi tại sao đánh giá lười biếng lại hữu ích. Tôi vẫn chưa có ai giải thích cho tôi theo cách có lý; chủ yếu là nó kết thúc để "tin tưởng tôi".

Lưu ý: Ý tôi không phải là ghi nhớ.

Câu trả lời:


96

Chủ yếu là vì nó có thể hiệu quả hơn - các giá trị không cần phải được tính toán nếu chúng không được sử dụng. Ví dụ: tôi có thể chuyển ba giá trị vào một hàm, nhưng tùy thuộc vào chuỗi biểu thức điều kiện, chỉ một tập con thực sự có thể được sử dụng. Trong một ngôn ngữ như C, tất cả ba giá trị sẽ được tính toán bằng mọi cách; nhưng trong Haskell, chỉ những giá trị cần thiết mới được tính toán.

Nó cũng cho phép những thứ thú vị như danh sách vô hạn. Tôi không thể có một danh sách vô hạn trong một ngôn ngữ như C, nhưng với Haskell, điều đó không có vấn đề gì. Danh sách vô hạn được sử dụng khá thường xuyên trong các lĩnh vực toán học nhất định, vì vậy có thể hữu ích nếu có khả năng vận dụng chúng.


6
Python có danh sách vô hạn được đánh giá một cách lười biếng thông qua trình vòng lặp
Mark Cidade

4
Bạn thực sự có thể bắt chước một danh sách vô hạn bằng Python sử dụng máy phát điện và máy phát biểu (mà làm việc theo một cách tương tự như một sự hiểu biết danh sách): python.org/doc/2.5.2/ref/genexpr.html
John Montgomery

24
Trình tạo tạo danh sách lười dễ dàng trong Python, nhưng các kỹ thuật đánh giá lười biếng và cấu trúc dữ liệu khác kém thanh lịch hơn đáng kể.
Peter Burns

3
Tôi e rằng tôi không đồng ý với câu trả lời này. Tôi đã từng nghĩ rằng lười biếng là do hiệu quả, nhưng sau khi sử dụng Haskell một lượng đáng kể, sau đó chuyển sang Scala và so sánh trải nghiệm, tôi phải nói rằng lười biếng thường xuyên quan trọng nhưng hiếm khi vì hiệu quả. Tôi nghĩ Edward Kmett đánh vào những lý do thực sự.
Owen

3
Tương tự, tôi không đồng ý, mặc dù không có khái niệm rõ ràng về danh sách vô hạn trong C vì đánh giá nghiêm ngặt, bạn có thể dễ dàng chơi cùng một thủ thuật trong bất kỳ ngôn ngữ nào khác (và thực sự, trong hầu hết mọi ngôn ngữ lười biếng thực tế) bằng cách sử dụng hàm thu và chuyển xung quanh hàm con trỏ làm việc với tiền tố hữu hạn của cấu trúc vô hạn được tạo ra bởi các biểu thức tương tự.
Kristopher Micinski

71

Một ví dụ hữu ích về đánh giá lười biếng là sử dụng quickSort:

quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)

Nếu bây giờ chúng ta muốn tìm mức tối thiểu của danh sách, chúng ta có thể xác định

minimum ls = head (quickSort ls)

Đầu tiên sắp xếp danh sách và sau đó lấy phần tử đầu tiên của danh sách. Tuy nhiên, vì lười đánh giá nên chỉ tính được cái đầu. Ví dụ, nếu chúng ta lấy mức tối thiểu của danh sách thì [2, 1, 3,]quickSort trước tiên sẽ lọc ra tất cả các phần tử nhỏ hơn hai. Sau đó, nó thực hiện QuickSort trên đó (trả về danh sách singleton [1]) là đã đủ. Vì lười đánh giá, phần còn lại không bao giờ được sắp xếp, tiết kiệm rất nhiều thời gian tính toán.

Tất nhiên đây là một ví dụ rất đơn giản, nhưng sự lười biếng hoạt động theo cách tương tự đối với các chương trình rất lớn.

Tuy nhiên, có một nhược điểm của tất cả điều này: việc dự đoán tốc độ thời gian chạy và mức sử dụng bộ nhớ của chương trình trở nên khó khăn hơn. Điều này không có nghĩa là các chương trình lười biếng sẽ chậm hơn hoặc chiếm nhiều bộ nhớ hơn, nhưng bạn nên biết.


19
Tổng quát hơn, take k $ quicksort listchỉ mất O (n + k log k) thời gian, ở đâu n = length list. Với cách sắp xếp so sánh không lười biếng, điều này sẽ luôn mất O (n log n) thời gian.
ephemient

@ephemient không phải ý bạn là O (nk log k)?
MaiaVictor

1
@Viclib Không, ý tôi là những gì tôi đã nói.
ephemient

@ephemient thì tôi nghĩ là tôi không hiểu, thật đáng buồn
MaiaVictor

2
@Viclib Một thuật toán lựa chọn để tìm k phần tử hàng đầu trong số n là O (n + k log k). Khi bạn triển khai quicksort bằng ngôn ngữ lười biếng và chỉ đánh giá nó đủ xa để xác định k phần tử đầu tiên (dừng đánh giá sau), nó sẽ tạo ra các so sánh chính xác giống như thuật toán lựa chọn không lười biếng.
ephemient

70

Tôi thấy đánh giá lười biếng hữu ích cho một số thứ.

Đầu tiên, tất cả các ngôn ngữ lười biếng hiện tại đều thuần túy, bởi vì rất khó để lý giải về các tác dụng phụ trong ngôn ngữ lười biếng.

Các ngôn ngữ thuần túy cho phép bạn lập luận về các định nghĩa hàm bằng cách sử dụng lập luận theo phương pháp tương đương.

foo x = x + 3

Thật không may trong cài đặt không lười biếng, nhiều câu lệnh không trả về hơn trong cài đặt lười biếng, vì vậy điều này ít hữu ích hơn trong các ngôn ngữ như ML. Nhưng bằng một ngôn ngữ lười biếng, bạn có thể lập luận một cách an toàn về sự bình đẳng.

Thứ hai, rất nhiều thứ như 'giới hạn giá trị' trong ML không cần thiết trong các ngôn ngữ lười biếng như Haskell. Điều này dẫn đến một sự khai báo tuyệt vời về cú pháp. ML như các ngôn ngữ cần sử dụng các từ khóa như var hoặc fun. Ở Haskell, những điều này thu gọn lại thành một khái niệm.

Thứ ba, sự lười biếng cho phép bạn viết mã rất chức năng có thể hiểu được từng phần. Trong Haskell, người ta thường viết một phần thân hàm như:

foo x y = if condition1
          then some (complicated set of combinators) (involving bigscaryexpression)
          else if condition2
          then bigscaryexpression
          else Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

Điều này cho phép bạn làm việc 'từ trên xuống' thông qua sự hiểu biết về phần thân của một hàm. Các ngôn ngữ giống ML buộc bạn phải sử dụng một ngôn ngữ letđược đánh giá nghiêm ngặt. Do đó, bạn không dám 'nâng' mệnh đề let ra phần chính của hàm, bởi vì nếu nó đắt tiền (hoặc có tác dụng phụ) thì bạn không muốn nó luôn được đánh giá. Haskell có thể 'đẩy' các chi tiết sang mệnh đề where một cách rõ ràng vì nó biết rằng nội dung của mệnh đề đó sẽ chỉ được đánh giá khi cần thiết.

Trong thực tế, chúng ta có xu hướng sử dụng lính canh và thu gọn chúng hơn nữa để:

foo x y 
  | condition1 = some (complicated set of combinators) (involving bigscaryexpression)
  | condition2 = bigscaryexpression
  | otherwise  = Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

Thứ tư, sự lười biếng đôi khi mang lại sự biểu đạt thanh lịch hơn nhiều đối với một số thuật toán nhất định. 'Sắp xếp nhanh' lười biếng trong Haskell là một cách đơn giản và có lợi ích là nếu bạn chỉ nhìn vào một vài mục đầu tiên, bạn chỉ phải trả chi phí tương ứng với chi phí chỉ chọn những mục đó. Không có gì ngăn cản bạn thực hiện điều này một cách nghiêm túc, nhưng bạn có thể phải mã hóa lại thuật toán mỗi lần để đạt được hiệu suất tiệm cận giống nhau.

Thứ năm, sự lười biếng cho phép bạn xác định cấu trúc điều khiển mới trong ngôn ngữ. Bạn không thể viết một 'if .. then .. else ..' mới như cấu trúc trong một ngôn ngữ nghiêm ngặt. Nếu bạn cố gắng xác định một hàm như:

if' True x y = x
if' False x y = y

bằng một ngôn ngữ nghiêm ngặt thì cả hai nhánh sẽ được đánh giá bất kể giá trị điều kiện. Nó trở nên tồi tệ hơn khi bạn xem xét các vòng lặp. Tất cả các giải pháp nghiêm ngặt yêu cầu ngôn ngữ cung cấp cho bạn một số loại báo giá hoặc cấu trúc lambda rõ ràng.

Cuối cùng, trong cùng mạch đó, một số cơ chế tốt nhất để đối phó với các tác dụng phụ trong hệ thống loại, chẳng hạn như monads, thực sự chỉ có thể được thể hiện hiệu quả trong một thiết lập lười biếng. Điều này có thể được chứng kiến ​​bằng cách so sánh mức độ phức tạp của Quy trình làm việc của F # với Đơn vị Haskell. (Bạn có thể định nghĩa một đơn nguyên bằng một ngôn ngữ nghiêm ngặt, nhưng không may là bạn sẽ thường xuyên trượt một hoặc hai luật đơn nguyên do thiếu sự lười biếng và Quy trình làm việc bằng cách so sánh có rất nhiều hành lý nghiêm ngặt.)


5
Rất đẹp; đây là những câu trả lời thực sự. Tôi từng nghĩ rằng đó là về hiệu quả (trì hoãn việc tính toán sau này) cho đến khi tôi sử dụng Haskell một lượng đáng kể và thấy rằng đó thực sự không phải là lý do.
Owen

11
Ngoài ra, mặc dù không đúng về mặt kỹ thuật rằng một ngôn ngữ lười biếng phải trong sáng (R là một ví dụ), nhưng đúng là một ngôn ngữ lười biếng không tinh khiết có thể làm những điều rất kỳ lạ (R làm ví dụ).
Owen

4
Chắc chắn là có. Trong một ngôn ngữ nghiêm ngặt, đệ quy letlà một con thú nguy hiểm, trong lược đồ R6RS, nó cho phép ngẫu nhiên #fxuất hiện trong thuật ngữ của bạn ở bất cứ nơi nào thắt chặt nút dẫn đến một chu kỳ! Không có ý định chơi chữ, nhưng các letràng buộc đệ quy chặt chẽ hơn là hợp lý trong một ngôn ngữ lười biếng. Tính nghiêm ngặt cũng làm trầm trọng thêm thực tế là wherekhông có cách nào để sắp xếp các hiệu ứng tương đối, ngoại trừ SCC, đó là một cấu trúc cấp độ tuyên bố, các tác động của nó có thể xảy ra theo bất kỳ thứ tự nghiêm ngặt nào và ngay cả khi bạn có một ngôn ngữ thuần túy, bạn kết hợp với #fvấn đề. Rắc rối nghiêm ngặt wheremã của bạn với các mối quan tâm không phải cục bộ.
Edward KMETT

2
Bạn có thể giải thích cách lười biếng giúp tránh bị giới hạn giá trị? Tôi đã không thể hiểu điều này.
Tom Ellis

3
@PaulBone Bạn đang nói gì vậy? Sự lười biếng có ảnh hưởng rất nhiều đến cấu trúc điều khiển. Nếu bạn xác định cấu trúc điều khiển của riêng mình bằng một ngôn ngữ chặt chẽ, nó sẽ phải sử dụng một loạt các lambdas hoặc tương tự, hoặc nó sẽ rất tệ. Bởi vì ifFunc(True, x, y)sẽ đánh giá cả hai xythay vì chỉ x.
dấu chấm phẩy

28

Có sự khác biệt giữa đánh giá đơn hàng bình thường với đánh giá lười biếng (như trong Haskell).

square x = x * x

Đánh giá biểu thức sau ...

square (square (square 2))

... với đánh giá háo hức:

> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256

... với đánh giá đơn hàng bình thường:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256

... với đánh giá lười biếng:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256

Đó là vì đánh giá lười biếng nhìn vào cây cú pháp và thực hiện các phép biến đổi cây ...

square (square (square 2))

           ||
           \/

           *
          / \
          \ /
    square (square 2)

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
        square 2

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
           *
          / \
          \ /
           2

... trong khi đánh giá thứ tự bình thường chỉ thực hiện mở rộng văn bản.

Đó là lý do tại sao chúng ta, khi sử dụng đánh giá lười biếng, sẽ có tác dụng mạnh hơn (đánh giá kết thúc thường xuyên hơn so với các chiến lược khác) trong khi hiệu suất tương đương với đánh giá háo hức (ít nhất là trong ký hiệu O).


25

Đánh giá lười biếng liên quan đến CPU giống như cách thu thập rác liên quan đến RAM. GC cho phép bạn giả vờ rằng bạn có số lượng bộ nhớ không giới hạn và do đó yêu cầu bao nhiêu đối tượng trong bộ nhớ nếu bạn cần. Runtime sẽ tự động lấy lại các đối tượng không sử dụng được. LE cho phép bạn giả vờ rằng bạn có tài nguyên tính toán không giới hạn - bạn có thể thực hiện nhiều phép tính nếu bạn cần. Runtime sẽ không thực thi các phép tính không cần thiết (đối với trường hợp cụ thể).

Ưu điểm thực tế của những mô hình "giả danh" này là gì? Nó giải phóng nhà phát triển (ở một mức độ nào đó) khỏi việc quản lý tài nguyên và xóa một số mã soạn sẵn khỏi nguồn của bạn. Nhưng quan trọng hơn là bạn có thể tái sử dụng giải pháp của mình một cách hiệu quả trong nhiều bối cảnh hơn.

Hãy tưởng tượng rằng bạn có một danh sách các số S và một số N. Bạn cần tìm gần nhất với số N số M từ danh sách S. Bạn có thể có hai ngữ cảnh: đơn N và một số danh sách L gồm N (ei cho mỗi N trong L bạn tìm M gần nhất trong S). Nếu bạn sử dụng đánh giá lười biếng, bạn có thể sắp xếp S và áp dụng tìm kiếm nhị phân để tìm M gần nhất với N. Để phân loại lười biếng tốt, nó sẽ yêu cầu các bước O (size (S)) cho N và O (ln (size (S)) * (size (S) + size (L))) các bước cho L. Nếu bạn không có đánh giá lười biếng để đạt được hiệu quả tối ưu, bạn phải triển khai thuật toán cho từng ngữ cảnh.


Việc tương tự với GC đã giúp tôi một chút, nhưng bạn có thể cho ví dụ về "loại bỏ một số mã viết sẵn" được không?
Abdul

1
@Abdul, một ví dụ quen thuộc với bất kỳ người dùng ORM nào: tải liên kết lười biếng. Nó tải liên kết từ DB "đúng lúc" và đồng thời giải phóng nhà phát triển khỏi sự cần thiết phải chỉ định rõ ràng khi nào tải nó và cách lưu nó vào bộ nhớ cache (ý tôi là đây là bản soạn sẵn). Đây là một ví dụ khác: projectlombok.org/features/GetterLazy.html .
Alexey

25

Nếu bạn tin rằng Simon Peyton Jones, đánh giá lười biếng là không quan trọng cho mỗi gia nhập nhưng chỉ như là một 'áo tóc' mà buộc các nhà thiết kế để giữ cho các ngôn ngữ thuần túy. Tôi thấy mình thông cảm với quan điểm này.

Richard Bird, John Hughes, và ở mức độ thấp hơn là Ralf Hinze có thể làm những điều đáng kinh ngạc với sự đánh giá lười biếng. Đọc tác phẩm của họ sẽ giúp bạn đánh giá cao nó. Để xuất phát điểm tốt là bộ giải Sudoku tuyệt vời của Bird và bài báo của Hughes về Các vấn đề lập trình chức năng .


Nó không chỉ buộc họ để giữ ngôn ngữ thuần túy, nó cũng chỉ cho phép họ làm như vậy, khi (trước sự ra đời của IOđơn nguyên) có chữ ký của mainsẽ String -> Stringvà bạn đã có thể viết chương trình đúng cách tương tác.
bùng binh bên trái

@leftaroundabout: Điều gì ngăn một ngôn ngữ nghiêm ngặt buộc tất cả các hiệu ứng vào một IOđơn nguyên?
Tom Ellis

13

Hãy xem xét một chương trình tic-tac-toe. Điều này có bốn chức năng:

  • Một chức năng tạo bảng di chuyển lấy một bảng hiện tại và tạo danh sách các bảng mới, mỗi bảng được áp dụng một lần di chuyển.
  • Sau đó, có một chức năng "cây di chuyển" áp dụng chức năng tạo di chuyển để lấy tất cả các vị trí bảng có thể có theo sau từ vị trí này.
  • Có một chức năng minimax đi bộ trên cây (hoặc có thể chỉ một phần của nó) để tìm ra nước đi tiếp theo tốt nhất.
  • Có một chức năng đánh giá bảng xác định xem một trong những người chơi có thắng hay không.

Điều này tạo ra sự tách biệt rõ ràng về các mối quan tâm. Đặc biệt, chức năng tạo nước đi và chức năng đánh giá bảng là những chức năng duy nhất cần hiểu luật chơi: cây di chuyển và chức năng minimax hoàn toàn có thể sử dụng lại được.

Bây giờ chúng ta hãy thử triển khai cờ vua thay vì tic-tac-toe. Trong ngôn ngữ "háo hức" (tức là thông thường), điều này sẽ không hoạt động vì cây di chuyển sẽ không phù hợp với bộ nhớ. Vì vậy, bây giờ các chức năng đánh giá bảng và tạo nước đi cần phải được trộn lẫn với cây di chuyển và logic minimax vì logic minimax phải được sử dụng để quyết định nước đi nào sẽ tạo ra. Cấu trúc mô-đun sạch đẹp của chúng tôi biến mất.

Tuy nhiên trong ngôn ngữ lười biếng, các phần tử của cây di chuyển chỉ được tạo ra để đáp ứng các yêu cầu từ hàm minimax: toàn bộ cây di chuyển không cần phải được tạo trước khi chúng ta để minimax rời khỏi phần tử trên cùng. Vì vậy, cấu trúc mô-đun sạch của chúng tôi vẫn hoạt động trong một trò chơi thực.


1
[Trong ngôn ngữ "háo hức" (tức là thông thường), điều này sẽ không hoạt động vì cây di chuyển sẽ không phù hợp với bộ nhớ] - đối với Tic-Tac-Toe thì chắc chắn là như vậy. Có nhiều nhất 3 ** 9 = 19683 vị trí cần lưu trữ. Nếu chúng tôi lưu trữ mỗi cái trong một dung lượng lớn 50 byte, thì đó là gần một megabyte. Đó là không có gì ...
Jonas Kölker

6
Vâng, đó là quan điểm của tôi. Ngôn ngữ háo hức có thể có cấu trúc rõ ràng cho các trò chơi tầm thường, nhưng phải thỏa hiệp cấu trúc đó cho bất kỳ thứ gì thực sự. Những ngôn ngữ lười biếng không có vấn đề đó.
Paul Johnson

3
Công bằng mà nói, việc lười đánh giá có thể dẫn đến các vấn đề về bộ nhớ của chính nó. Đó không phải là không phổ biến cho mọi người hỏi tại sao Haskell là thổi ra khỏi bộ nhớ của nó kiếm cái gì đó, trong một đánh giá háo hức, sẽ có mức tiêu thụ bộ nhớ của O (1)
RHSeeger

@PaulJohnson Nếu bạn đánh giá tất cả các vị trí, không có gì khác biệt cho dù bạn đánh giá chúng một cách háo hức hay lười biếng. Công việc tương tự phải được thực hiện. Nếu bạn dừng lại giữa chừng và chỉ đánh giá một nửa số vị trí, điều đó cũng không tạo ra sự khác biệt nào, bởi vì trong cả hai trường hợp, một nửa công việc phải hoàn thành. Sự khác biệt duy nhất giữa hai đánh giá là thuật toán trông đẹp hơn, nếu được viết một cách lười biếng.
ngừng

12

Đây là hai điểm nữa mà tôi không tin đã được đưa ra trong cuộc thảo luận.

  1. Sự lười biếng là một cơ chế đồng bộ hóa trong một môi trường đồng thời. Đây là một cách nhẹ và dễ dàng để tạo tham chiếu đến một số phép tính và chia sẻ kết quả của nó giữa nhiều luồng. Nếu nhiều luồng cố gắng truy cập một giá trị không được đánh giá, chỉ một trong số chúng sẽ thực thi nó và những luồng khác sẽ chặn theo đó, nhận giá trị khi nó có sẵn.

  2. Sự lười biếng là điều cơ bản để phân bổ cấu trúc dữ liệu trong một môi trường thuần túy. Điều này được Okasaki mô tả chi tiết trong Cấu trúc dữ liệu chức năng thuần túy , nhưng ý tưởng cơ bản là đánh giá lười biếng là một dạng đột biến có kiểm soát rất quan trọng để cho phép chúng ta triển khai một số loại cấu trúc dữ liệu một cách hiệu quả. Trong khi chúng ta thường nói về sự lười biếng buộc chúng ta phải mặc chiếc áo sơ mi thuần khiết, thì một cách khác cũng được áp dụng: chúng là một cặp đặc điểm ngôn ngữ hiệp đồng.


10

Khi bạn bật máy tính của mình và Windows sẽ không mở từng thư mục trên ổ cứng trong Windows Explorer và không khởi chạy mọi chương trình được cài đặt trên máy tính của bạn, cho đến khi bạn chỉ ra rằng cần một thư mục nhất định hoặc một chương trình nhất định, điều đó là đánh giá "lười biếng".

Đánh giá "lười biếng" đang thực hiện các hoạt động khi cần thiết. Nó hữu ích khi nó là một tính năng của ngôn ngữ lập trình hoặc thư viện vì nói chung việc tự thực hiện đánh giá lười biếng sẽ khó hơn là chỉ tính toán trước mọi thứ.


1
Một số người có thể nói rằng đó thực sự là "thực thi lười biếng". Sự khác biệt thực sự là phi vật chất ngoại trừ trong các ngôn ngữ thuần túy hợp lý như Haskell; nhưng sự khác biệt là nó không chỉ bị trì hoãn tính toán mà còn có các tác dụng phụ liên quan đến nó (như mở và đọc tệp).
Owen

8

Xem xét điều này:

if (conditionOne && conditionTwo) {
  doSomething();
}

Phương thức doSomething () sẽ chỉ được thực thi nếu conditionOne là true conditionTwo là true. Trong trường hợp conditionOne là false, tại sao bạn cần tính kết quả của conditionTwo? Việc đánh giá điều kiện Hai sẽ là một sự lãng phí thời gian trong trường hợp này, đặc biệt nếu tình trạng của bạn là kết quả của một quá trình phương pháp nào đó.

Đó là một ví dụ về sở thích đánh giá lười biếng ...


Tôi nghĩ rằng đó là sự thiếu hụt, không phải là lười biếng đánh giá.
Thomas Owens

2
Đó là đánh giá lười biếng vì điều kiện Hai chỉ được tính nếu nó thực sự cần thiết (tức là nếu điều kiện Một là đúng).
Romain Linsolas

7
Tôi cho rằng đoản mạch là một trường hợp suy thoái của việc đánh giá lười biếng, nhưng chắc chắn đó không phải là cách thông thường để nghĩ về nó.
rmeador

19
Chập mạch thực chất là một trường hợp đặc biệt của việc lười đánh giá. Rõ ràng là đánh giá lười biếng bao gồm nhiều thứ hơn là chỉ đoản mạch. Hoặc, đoản mạch có gì trên và đánh giá lười biếng?
yfeldblum

2
@Juliet: Bạn định nghĩa rõ ràng về 'lười biếng'. Ví dụ của bạn về một hàm nhận hai tham số không giống như một câu lệnh if ngắn mạch. Câu lệnh if ngắn mạch tránh được các tính toán không cần thiết. Tôi nghĩ rằng một sự so sánh tốt hơn với ví dụ của bạn sẽ là nhà điều hành Visual Basic của "AndAlso" mà lực lượng cả hai điều kiện để được đánh giá

8
  1. Nó có thể tăng hiệu quả. Đây là cái nhìn rõ ràng, nhưng nó không thực sự quan trọng nhất. (Cũng lưu ý rằng sự lười biếng cũng có thể giết chết hiệu quả - điều này không rõ ràng ngay lập tức. Tuy nhiên, bằng cách lưu trữ nhiều kết quả tạm thời thay vì tính toán chúng ngay lập tức, bạn có thể sử dụng một lượng lớn RAM).

  2. Nó cho phép bạn xác định các cấu trúc điều khiển luồng trong mã cấp người dùng thông thường, thay vì nó được mã hóa cứng sang ngôn ngữ. (Ví dụ: Java có forcác vòng lặp; Haskell có một forhàm. Java có xử lý ngoại lệ; Haskell có nhiều loại đơn nguyên ngoại lệ. C # có goto; Haskell có đơn nguyên tiếp tục ...)

  3. Nó cho phép bạn tách thuật toán tạo dữ liệu từ thuật toán để quyết định lượng dữ liệu cần tạo. Bạn có thể viết một hàm tạo ra một danh sách kết quả có vô hạn và một hàm khác xử lý càng nhiều danh sách này khi nó quyết định nó cần. Hơn nữa, bạn có thể có năm chức năng máy phát điện và năm chức năng tiêu dùng và bạn có thể tạo ra bất kỳ tổ hợp nào một cách hiệu quả - thay vì mã hóa thủ công 5 x 5 = 25 chức năng kết hợp cả hai hành động cùng một lúc. (!) Tất cả chúng ta đều biết tách là một điều tốt.

  4. Nó ít nhiều buộc bạn phải thiết kế một ngôn ngữ chức năng thuần túy . Nó luôn luôn hấp dẫn để có các phím tắt, nhưng trong một ngôn ngữ lười biếng, các tạp chất nhỏ nhất làm cho mã của bạn một cách hoang dại không thể đoán trước, mà mạnh mẽ đã phản dùng phím tắt.


6

Một lợi ích to lớn của sự lười biếng là khả năng viết cấu trúc dữ liệu bất biến với giới hạn phân bổ hợp lý. Một ví dụ đơn giản là ngăn xếp bất biến (sử dụng F #):

type 'a stack =
    | EmptyStack
    | StackNode of 'a * 'a stack

let rec append x y =
    match x with
    | EmptyStack -> y
    | StackNode(hd, tl) -> StackNode(hd, append tl y)

Mã là hợp lý, nhưng việc thêm hai ngăn xếp x và y sẽ mất O (độ dài của x) trong các trường hợp tốt nhất, xấu nhất và trung bình. Thêm hai ngăn xếp là một hoạt động đơn nguyên, nó chạm vào tất cả các nút trong ngăn xếp x.

Chúng ta có thể viết lại cấu trúc dữ liệu dưới dạng một ngăn xếp lười biếng:

type 'a lazyStack =
    | StackNode of Lazy<'a * 'a lazyStack>
    | EmptyStack

let rec append x y =
    match x with
    | StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
    | Empty -> y

lazyhoạt động bằng cách tạm ngừng đánh giá mã trong phương thức khởi tạo của nó. Sau khi được đánh giá bằng cách sử dụng .Force(), giá trị trả về được lưu vào bộ nhớ đệm và sử dụng lại vào mỗi lần tiếp theo .Force().

Với phiên bản lười biếng, phần bổ sung là một hoạt động O (1): nó trả về 1 nút và tạm dừng việc xây dựng lại danh sách thực tế. Khi bạn nhận được phần đầu của danh sách này, nó sẽ đánh giá nội dung của nút, buộc nó trả lại phần đầu và tạo một hệ thống treo với các phần tử còn lại, vì vậy việc lấy phần đầu của danh sách là một phép toán O (1).

Vì vậy, danh sách lười biếng của chúng tôi đang ở trạng thái xây dựng lại liên tục, bạn không phải trả chi phí cho việc xây dựng lại danh sách này cho đến khi bạn xem qua tất cả các phần tử của nó. Sử dụng sự lười biếng, danh sách này ủng hộ sự đồng tình và phụ thuộc của O (1). Điều thú vị là, vì chúng tôi không đánh giá các nút cho đến khi chúng được truy cập, nên hoàn toàn có thể xây dựng một danh sách với các phần tử có khả năng vô hạn.

Cấu trúc dữ liệu ở trên không yêu cầu các nút phải được tính toán lại trên mỗi lần truyền tải, vì vậy chúng khác biệt rõ ràng với IEnumerables vani trong .NET.


5

Đoạn mã này cho thấy sự khác biệt giữa đánh giá lười biếng và không lười biếng. Tất nhiên, bản thân hàm fibonacci này có thể được tối ưu hóa và sử dụng đánh giá lười biếng thay vì đệ quy, nhưng điều đó sẽ làm hỏng ví dụ.

Giả sử chúng ta CÓ THỂ phải sử dụng 20 số đầu tiên cho một thứ gì đó, với đánh giá không lười biếng, tất cả 20 số phải được tạo trước, nhưng với đánh giá lười biếng, chúng sẽ chỉ được tạo khi cần thiết. Vì vậy, bạn sẽ chỉ phải trả giá tính toán khi cần thiết.

Đầu ra mẫu

Không phải thế hệ lười biếng: 0,023373
Thế hệ lười biếng: 0,000009
Đầu ra không lười biếng: 0,000921
Đầu ra lười biếng: 0,024205
import time

def now(): return time.time()

def fibonacci(n): #Recursion for fibonacci (not-lazy)
 if n < 2:
  return n
 else:
  return fibonacci(n-1)+fibonacci(n-2)

before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()


before3 = now()
for i in notlazy:
  print i
after3 = now()

before4 = now()
for i in lazy:
  print i
after4 = now()

print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)

5

Đánh giá lười biếng là hữu ích nhất với cấu trúc dữ liệu. Bạn có thể xác định một cách cảm tính một mảng hoặc vectơ chỉ xác định một số điểm nhất định trong cấu trúc và thể hiện tất cả những điểm khác theo toàn bộ mảng. Điều này cho phép bạn tạo cấu trúc dữ liệu rất ngắn gọn và với hiệu suất thời gian chạy cao.

Để thấy điều này trong thực tế, bạn có thể xem thư viện mạng thần kinh của tôi có tên là bản năng . Nó sử dụng nhiều đánh giá lười biếng cho sự sang trọng và hiệu suất cao. Ví dụ, tôi hoàn toàn loại bỏ cách tính kích hoạt bắt buộc truyền thống. Một biểu hiện lười biếng đơn giản làm mọi thứ cho tôi.

Ví dụ: điều này được sử dụng trong hàm kích hoạt và cả trong thuật toán học tập lan truyền ngược (tôi chỉ có thể đăng hai liên kết, vì vậy bạn sẽ cần phải tự mình tra cứu learnPathàm trong AI.Instinct.Train.Deltamô-đun). Theo truyền thống, cả hai đều yêu cầu các thuật toán lặp lại phức tạp hơn nhiều.


4

Những người khác đã đưa ra tất cả các lý do lớn, nhưng tôi nghĩ rằng một bài tập hữu ích để giúp hiểu lý do tại sao sự lười biếng lại quan trọng là thử và viết một điểm cố định hàm bằng một ngôn ngữ nghiêm ngặt.

Trong Haskell, một hàm điểm cố định rất dễ dàng:

fix f = f (fix f)

điều này mở rộng thành

f (f (f ....

nhưng bởi vì Haskell lười biếng, chuỗi tính toán vô hạn đó không có vấn đề gì; đánh giá được thực hiện "từ ngoài vào trong" và mọi thứ hoạt động tuyệt vời:

fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)

Quan trọng là, vấn đề không phải fixlà lười biếng, mà flà lười biếng. Một khi bạn đã được đưa ra một sự nghiêm khắc f, bạn có thể giơ tay lên và bỏ cuộc, hoặc mở rộng nó ra và làm lộn xộn mọi thứ. (Điều này rất giống với những gì Noah đã nói về việc thư viện nghiêm ngặt / lười biếng, không phải ngôn ngữ).

Bây giờ hãy tưởng tượng viết cùng một hàm trong Scala nghiêm ngặt:

def fix[A](f: A => A): A = f(fix(f))

val fact = fix[Int=>Int] { f => n =>
    if (n == 0) 1
    else n*f(n-1)
}

Tất nhiên bạn sẽ nhận được tràn ngăn xếp. Nếu bạn muốn nó hoạt động, bạn cần thực hiện fgọi đối số theo nhu cầu:

def fix[A](f: (=>A) => A): A = f(fix(f))

def fact1(f: =>Int=>Int) = (n: Int) =>
    if (n == 0) 1
    else n*f(n-1)

val fact = fix(fact1)

3

Tôi không biết hiện tại bạn nghĩ về mọi thứ như thế nào, nhưng tôi thấy hữu ích khi coi đánh giá lười biếng như một vấn đề thư viện hơn là một tính năng ngôn ngữ.

Ý tôi là trong các ngôn ngữ nghiêm ngặt, tôi có thể thực hiện đánh giá lười biếng bằng cách xây dựng một vài cấu trúc dữ liệu và trong các ngôn ngữ lười biếng (ít nhất là Haskell), tôi có thể yêu cầu tính nghiêm ngặt khi tôi muốn. Do đó, lựa chọn ngôn ngữ không thực sự làm cho chương trình của bạn lười biếng hoặc không lười biếng, mà chỉ đơn giản là ảnh hưởng đến những gì bạn nhận được theo mặc định.

Một khi bạn nghĩ về nó như vậy, hãy nghĩ đến tất cả những nơi bạn viết cấu trúc dữ liệu mà sau này bạn có thể sử dụng để tạo dữ liệu (mà không cần xem xét nó quá nhiều trước đó), và bạn sẽ thấy rất nhiều công dụng cho sự lười biếng đánh giá.


1
Việc thực hiện đánh giá lười biếng bằng các ngôn ngữ nghiêm ngặt thường là một Turing Tarpit.
itbruce,

2

Cách khai thác hữu ích nhất của đánh giá lười biếng mà tôi đã sử dụng là một hàm gọi là một loạt các hàm con theo một thứ tự cụ thể. Nếu bất kỳ một trong các hàm con này bị lỗi (trả về false), hàm gọi cần trả về ngay lập tức. Vì vậy, tôi có thể đã làm theo cách này:

bool Function(void) {
  if (!SubFunction1())
    return false;
  if (!SubFunction2())
    return false;
  if (!SubFunction3())
    return false;

(etc)

  return true;
}

hoặc, giải pháp thanh lịch hơn:

bool Function(void) {
  if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
    return false;
  return true;
}

Khi bạn bắt đầu sử dụng nó, bạn sẽ thấy cơ hội sử dụng nó nhiều hơn và thường xuyên hơn.


2

Nếu không đánh giá lười biếng, bạn sẽ không được phép viết một cái gì đó như thế này:

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }

Vâng, imo, đây là một ý tưởng tồi để làm điều đó. Mặc dù mã này có thể đúng (tùy thuộc vào những gì bạn cố gắng đạt được), nhưng nó rất khó đọc, điều này luôn là một điều tồi tệ.
Brann

12
Tôi không nghĩ vậy. Một cấu trúc tiêu chuẩn của nó trong C và họ hàng của nó.
Paul Johnson

Đây là một ví dụ về đánh giá ngắn mạch, không đánh giá lười biếng. Hay đó có phải là điều tương tự?
RufusVS

2

Trong số những thứ khác, ngôn ngữ lười biếng cho phép cấu trúc dữ liệu vô hạn đa chiều.

Mặc dù lược đồ, python, v.v. cho phép cấu trúc dữ liệu vô hạn một chiều với các luồng, bạn chỉ có thể duyệt dọc theo một chiều.

Sự lười biếng rất hữu ích cho cùng một vấn đề ngoài lề , nhưng cần lưu ý đến kết nối coroutines được đề cập trong liên kết đó.


2

Đánh giá lười biếng là lý luận cân bằng của con người kém cỏi (lý tưởng nhất có thể được mong đợi là suy ra các thuộc tính của mã từ các thuộc tính của các kiểu và phép toán liên quan).

Ví dụ nơi nó hoạt động khá tốt: sum . take 10 $ [1..10000000000] . Mà chúng tôi không ngại khi được rút gọn thành tổng 10 số, thay vì chỉ một phép tính số trực tiếp và đơn giản. Tất nhiên nếu không có sự đánh giá lười biếng, điều này sẽ tạo ra một danh sách khổng lồ trong bộ nhớ chỉ để sử dụng 10 phần tử đầu tiên của nó. Nó chắc chắn sẽ rất chậm và có thể gây ra lỗi hết bộ nhớ.

Ví dụ mà nó không lớn như chúng tôi muốn: sum . take 1000000 . drop 500 $ cycle [1..20]. Mà thực sự sẽ tính tổng 1 000 000 số, ngay cả khi trong một vòng lặp thay vì trong một danh sách; Tuy nhiên, nó nên được giảm xuống chỉ còn một phép tính số trực tiếp, với ít điều kiện và ít công thức. Vậy sẽ tốt hơn rất nhiều khi cộng tổng số 1 000 000. Ngay cả khi trong một vòng lặp, và không trong một danh sách (tức là sau khi tối ưu hóa phá rừng).


Một điều nữa là, nó làm cho nó có thể viết mã theo kiểu mô-đun đệ quy đuôi và nó chỉ hoạt động .

cf. câu trả lời liên quan .


1

Nếu bằng "đánh giá lười biếng", bạn có nghĩa là giống như trong boolean kết hợp, như trong

   if (ConditionA && ConditionB) ... 

thì câu trả lời đơn giản là chương trình tiêu thụ càng ít chu kỳ CPU, thì chương trình sẽ chạy càng nhanh ... và nếu một phần nhỏ các lệnh xử lý sẽ không ảnh hưởng đến kết quả của chương trình thì nó là không cần thiết, (và do đó, nó là một sự lãng phí thời gian) để thực hiện chúng bằng mọi cách ...

nếu otoh, ý bạn là tôi đã gọi là "bộ khởi tạo lười biếng", như trong:

class Employee
{
    private int supervisorId;
    private Employee supervisor;

    public Employee(int employeeId)
    {
        // code to call database and fetch employee record, and 
        //  populate all private data fields, EXCEPT supervisor
    }
    public Employee Supervisor
    { 
       get 
          { 
              return supervisor?? (supervisor = new Employee(supervisorId)); 
          } 
    }
}

Tốt, kỹ thuật này cho phép mã máy khách sử dụng lớp để tránh phải gọi cơ sở dữ liệu cho bản ghi dữ liệu Người giám sát ngoại trừ khi máy khách sử dụng đối tượng Nhân viên yêu cầu quyền truy cập vào dữ liệu của người giám sát ... điều này làm cho quá trình khởi tạo Nhân viên nhanh hơn, và khi bạn cần Người giám sát, lệnh gọi đầu tiên đến thuộc tính Người giám sát sẽ kích hoạt lệnh gọi Cơ sở dữ liệu và dữ liệu sẽ được tìm nạp và có sẵn ...


0

Trích từ các hàm bậc cao hơn

Hãy tìm số lớn nhất dưới 100.000 chia hết cho 3829. Để làm điều đó, chúng tôi sẽ chỉ lọc một tập hợp các khả năng mà chúng tôi biết lời giải nằm ở đâu.

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

Đầu tiên, chúng tôi lập danh sách tất cả các số thấp hơn 100.000, giảm dần. Sau đó, chúng tôi lọc nó theo vị từ của chúng tôi và bởi vì các số được sắp xếp theo cách giảm dần, số lớn nhất thỏa mãn vị từ của chúng tôi là phần tử đầu tiên của danh sách được lọc. Chúng tôi thậm chí không cần sử dụng một danh sách hữu hạn cho tập khởi đầu của mình. Lại là sự lười biếng trong hành động. Bởi vì chúng ta chỉ sử dụng phần đầu của danh sách đã lọc, nên không quan trọng nếu danh sách được lọc là hữu hạn hay vô hạn. Việc đánh giá dừng lại khi tìm thấy giải pháp thích hợp đầu tiên.

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.