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ớ.
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:
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.
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.
take k $ quicksort list
chỉ 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.
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.)
let
là một con thú nguy hiểm, trong lược đồ R6RS, nó cho phép ngẫu nhiên #f
xuấ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 let
rà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à where
khô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 #f
vấn đề. Rắc rối nghiêm ngặt where
mã của bạn với các mối quan tâm không phải cục bộ.
ifFunc(True, x, y)
sẽ đánh giá cả hai x
và y
thay vì chỉ x
.
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).
Đá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.
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 .
IO
đơn nguyên) có chữ ký của main
sẽ String -> String
và bạn đã có thể viết chương trình đúng cách tương tác.
IO
đơn nguyên?
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:
Đ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.
Đây là hai điểm nữa mà tôi không tin đã được đưa ra trong cuộc thảo luận.
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.
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.
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ứ.
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 và 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 ...
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).
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ó for
các vòng lặp; Haskell có một for
hà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 ...)
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.
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.
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
lazy
hoạ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.
Đ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)
Đá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 learnPat
hàm trong AI.Instinct.Train.Delta
mô-đ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.
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 fix
là lười biếng, mà f
là 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 f
gọ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)
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á.
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.
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
}
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 đó.
Đá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 .
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 ...
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.