Tại sao đánh giá lười biếng không được sử dụng ở mọi nơi?


32

Tôi vừa tìm hiểu cách đánh giá lười biếng hoạt động và tôi đã tự hỏi: tại sao đánh giá lười biếng không được áp dụng trong mọi phần mềm hiện được sản xuất? Tại sao vẫn sử dụng đánh giá háo hức?


2
Đây là một ví dụ về những gì có thể xảy ra nếu bạn trộn lẫn trạng thái đột biến và đánh giá lười biếng. alicebobandmallory.com/articles/2011/01/01/ khăn
Jonas Elfström

2
@ JonasElfström: Xin vui lòng, đừng nhầm lẫn trạng thái có thể thay đổi với một trong những triển khai có thể có của nó. Trạng thái có thể thay đổi có thể được thực hiện bằng cách sử dụng một luồng giá trị vô hạn, lười biếng. Sau đó, bạn không có vấn đề về các biến có thể thay đổi.
Giorgio

Trong các ngôn ngữ lập trình bắt buộc, "đánh giá lười biếng" đòi hỏi một nỗ lực có ý thức từ lập trình viên. Lập trình chung trong các ngôn ngữ bắt buộc đã làm điều này dễ dàng, nhưng nó sẽ không bao giờ minh bạch. Câu trả lời cho mặt khác của câu hỏi đưa ra một câu hỏi khác: "Tại sao ngôn ngữ lập trình chức năng không được sử dụng ở mọi nơi?", Và câu trả lời hiện tại chỉ đơn giản là "không" như một vấn đề hiện tại.
rwong

2
Các ngôn ngữ lập trình chức năng không được sử dụng ở mọi nơi vì cùng một lý do chúng tôi không sử dụng búa trên ốc vít, không phải mọi vấn đề đều có thể dễ dàng được thể hiện trong đầu vào chức năng -> cách đầu ra, ví dụ GUI phù hợp hơn để được thể hiện theo cách bắt buộc .
ALXGTV

Hơn nữa, có hai loại ngôn ngữ lập trình chức năng (hoặc ít nhất là cả hai đều khẳng định là chức năng), các ngôn ngữ chức năng bắt buộc như Clojure, Scala và khai báo, ví dụ như Haskell, OCaml.
ALXGTV

Câu trả lời:


38

Đánh giá lười biếng đòi hỏi phải giữ sổ sách - bạn phải biết nếu nó được đánh giá chưa và những thứ như vậy. Đánh giá háo hức luôn được đánh giá, vì vậy bạn không cần phải biết. Điều này đặc biệt đúng trong bối cảnh đồng thời.

Thứ hai, đó là tầm thường để chuyển đổi đánh giá háo hức vào đánh giá lười biếng bằng cách đóng gói nó vào một đối tượng hàm được gọi sau đó, nếu bạn muốn.

Thứ ba, đánh giá lười biếng hàm ý mất kiểm soát. Điều gì sẽ xảy ra nếu tôi lười biếng đánh giá việc đọc một tập tin từ đĩa? Hoặc nhận được thời gian? Điều đó không được chấp nhận.

Đánh giá háo hức có thể hiệu quả hơn và dễ kiểm soát hơn, và được chuyển đổi tầm thường thành đánh giá lười biếng. Tại sao bạn muốn đánh giá lười biếng?


10
Nhanh chóng đọc một tập tin từ đĩa thực sự rất gọn gàng - đối với hầu hết các chương trình và tập lệnh đơn giản của tôi, Haskell chính xácreadFile là những gì tôi cần. Bên cạnh đó, chuyển đổi từ đánh giá lười biếng sang háo hức cũng chỉ là chuyện nhỏ.
Tikhon Jelvis

3
Đồng ý với tất cả các bạn ngoại trừ đoạn cuối cùng. Đánh giá lười biếng hiệu quả hơn khi có hoạt động theo chuỗi và nó có thể kiểm soát nhiều hơn khi bạn thực sự cần dữ liệu
texasbruce

4
Các luật functor muốn có một lời với bạn về "mất kiểm soát". Nếu bạn viết các hàm thuần túy hoạt động trên các kiểu dữ liệu bất biến, đánh giá lười biếng là một ơn trời. Các ngôn ngữ như haskell về cơ bản dựa trên khái niệm về sự lười biếng. Nó cồng kềnh trong một số ngôn ngữ, đặc biệt là khi trộn lẫn với mã "không an toàn", nhưng bạn đang làm cho nó có vẻ như sự lười biếng là nguy hiểm hoặc xấu theo mặc định. Nó chỉ "nguy hiểm" trong mã nguy hiểm.
sara

1
@DeadMG Không phải nếu bạn quan tâm đến việc mã của bạn có chấm dứt hay không ... Điều gì head [1 ..]mang lại cho bạn một ngôn ngữ thuần túy được đánh giá một cách háo hức, bởi vì trong Haskell nó mang lại 1?
dấu chấm phẩy

1
Đối với nhiều ngôn ngữ, việc thực hiện đánh giá lười biếng ít nhất sẽ giới thiệu sự phức tạp. Đôi khi, sự phức tạp đó là cần thiết và việc đánh giá lười biếng sẽ cải thiện hiệu quả tổng thể - đặc biệt nếu những gì được đánh giá chỉ là điều kiện cần thiết. Tuy nhiên, được thực hiện kém, nó có thể đưa ra các lỗi tinh vi hoặc khó giải thích các vấn đề về hiệu suất do các giả định xấu khi viết mã. Có một sự đánh đổi.
Berin Loritsch

17

Chủ yếu là vì mã lười và trạng thái có thể trộn lẫn xấu và gây ra một số lỗi khó tìm. Nếu trạng thái của một đối tượng phụ thuộc thay đổi, giá trị của đối tượng lười biếng của bạn có thể sai khi được đánh giá. Sẽ tốt hơn nhiều nếu lập trình viên mã hóa rõ ràng đối tượng để lười biếng khi anh ấy / cô ấy biết tình huống này là phù hợp.

Mặt khác, Haskell sử dụng đánh giá Lazy cho mọi thứ. Điều này là có thể bởi vì đó là ngôn ngữ chức năng và không sử dụng trạng thái (ngoại trừ trong một vài trường hợp đặc biệt khi chúng được đánh dấu rõ ràng)


Yeah, trạng thái đột biến + đánh giá lười biếng = cái chết. Tôi nghĩ rằng những điểm duy nhất tôi đã mất trong trận chung kết SICP của mình là về việc sử dụng set!trình thông dịch Scheme lười biếng. > :(
Tikhon Jelvis

3
"Mã lười và trạng thái có thể trộn lẫn xấu": Nó thực sự phụ thuộc vào cách bạn thực hiện trạng thái. Nếu bạn thực hiện nó bằng cách sử dụng các biến có thể thay đổi được chia sẻ và bạn phụ thuộc vào thứ tự đánh giá để trạng thái của bạn nhất quán, thì bạn đã đúng.
Giorgio

14

Đánh giá lười biếng không phải lúc nào cũng tốt hơn.

Lợi ích về hiệu suất của đánh giá lười biếng có thể rất lớn, nhưng không khó để tránh hầu hết các đánh giá không cần thiết trong môi trường háo hức - chắc chắn lười biếng làm cho nó dễ dàng và đầy đủ, nhưng hiếm khi đánh giá không cần thiết trong mã là một vấn đề lớn.

Điều tốt về đánh giá lười biếng là khi nó cho phép bạn viết mã rõ ràng hơn; lấy số nguyên tố thứ 10 bằng cách lọc danh sách số tự nhiên vô hạn và lấy phần tử thứ 10 của danh sách đó là một trong những cách tiến hành ngắn gọn và rõ ràng nhất: (mã giả)

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

Tôi tin rằng sẽ rất khó để diễn đạt mọi thứ một cách chính xác mà không lười biếng.

Nhưng sự lười biếng không phải là câu trả lời cho mọi thứ. Đối với người mới bắt đầu, sự lười biếng không thể được áp dụng một cách minh bạch khi có trạng thái và tôi tin rằng trạng thái không thể được tự động phát hiện (trừ khi bạn đang làm việc nói, Haskell, khi trạng thái khá rõ ràng). Vì vậy, trong hầu hết các ngôn ngữ, sự lười biếng cần phải được thực hiện thủ công, điều này làm cho mọi thứ không rõ ràng và do đó loại bỏ một trong những lợi ích lớn của việc lười biếng.

Hơn nữa, sự lười biếng có những hạn chế về hiệu suất, vì nó phải chịu một chi phí đáng kể trong việc giữ các biểu thức không được đánh giá xung quanh; họ sử dụng hết dung lượng và họ làm việc chậm hơn so với các giá trị đơn giản. Không có gì lạ khi phát hiện ra rằng bạn phải háo hức mã vì phiên bản lười biếng là chó chậm - và đôi khi rất khó để lý giải về hiệu suất.

Vì nó có xu hướng xảy ra, không có chiến lược tốt nhất tuyệt đối. Lười là tuyệt vời nếu bạn có thể viết mã tốt hơn bằng cách tận dụng các cấu trúc dữ liệu vô hạn hoặc các chiến lược khác mà nó cho phép bạn sử dụng, nhưng háo hức có thể dễ dàng tối ưu hóa hơn.


Liệu một trình biên dịch thực sự thông minh có thể giảm thiểu chi phí đáng kể hay không. hoặc thậm chí tận dụng sự lười biếng để tối ưu hóa thêm?
Tikhon Jelvis

3

Dưới đây là một so sánh ngắn về ưu và nhược điểm của đánh giá háo hức và lười biếng:

  • Đánh giá háo hức:

    • Chi phí tiềm năng của công cụ đánh giá không cần thiết.

    • Vô tư, đánh giá nhanh.

  • Đánh giá lười biếng:

    • Không có đánh giá không cần thiết.

    • Sổ sách kế toán ở mỗi lần sử dụng một giá trị.

Vì vậy, nếu bạn có nhiều biểu hiện không bao giờ phải đánh giá, lười biếng là tốt hơn; Tuy nhiên, nếu bạn không bao giờ có một biểu thức không cần phải đánh giá, thì lười biếng hoàn toàn là chi phí.

Bây giờ, hãy xem phần mềm trong thế giới thực: Có bao nhiêu chức năng mà bạn viết không yêu cầu đánh giá tất cả các đối số của chúng? Đặc biệt với các hàm ngắn hiện đại chỉ làm một việc, tỷ lệ các hàm rơi vào loại này rất thấp. Do đó, đánh giá lười biếng sẽ chỉ giới thiệu phần lớn sổ sách kế toán, mà không có cơ hội thực sự tiết kiệm bất cứ điều gì.

Do đó, đánh giá lười biếng đơn giản là không trả tiền trung bình, đánh giá háo hức là phù hợp hơn cho mã hiện đại.


1
"Chi phí kế toán sổ sách mỗi khi sử dụng một giá trị.": Tôi không nghĩ rằng chi phí kế toán sổ sách lớn hơn, giả sử, kiểm tra các tài liệu tham khảo null trong một ngôn ngữ như Java. Trong cả hai trường hợp, bạn cần kiểm tra một bit thông tin (được đánh giá / đang chờ xử lý so với null / không null) và bạn cần thực hiện mỗi khi bạn sử dụng một giá trị. Vì vậy, vâng, có một chi phí, nhưng nó là tối thiểu.
Giorgio

1
"Có bao nhiêu hàm mà bạn viết không yêu cầu đánh giá tất cả các đối số của chúng?": Đây chỉ là một ứng dụng ví dụ. Những gì về cấu trúc dữ liệu đệ quy, vô hạn? Bạn có thể thực hiện chúng với đánh giá háo hức? Bạn có thể sử dụng các trình vòng lặp, nhưng giải pháp không phải lúc nào cũng ngắn gọn. Tất nhiên bạn có thể không bỏ lỡ một cái gì đó mà bạn chưa bao giờ có cơ hội sử dụng rộng rãi.
Giorgio

2
"Do đó, đánh giá lười biếng đơn giản là không trả tiền trung bình, đánh giá háo hức là phù hợp hơn với mã hiện đại.": Tuyên bố này không đúng: nó thực sự phụ thuộc vào những gì bạn đang cố gắng thực hiện.
Giorgio

1
@Giorgio Chi phí hoạt động có vẻ không nhiều đối với bạn, nhưng điều kiện là một trong những điều mà CPU hiện đại hút vào: Một nhánh bị dự đoán sai thường buộc phải xả hoàn toàn đường ống, loại bỏ công việc của hơn mười chu kỳ CPU. Bạn không muốn các điều kiện không cần thiết trong vòng lặp bên trong của bạn. Trả thêm mười chu kỳ cho mỗi đối số chức năng gần như không thể chấp nhận được đối với mã nhạy cảm hiệu năng như mã hóa điều trong java. Bạn đúng khi đánh giá lười biếng cho phép bạn rút ra một số thủ thuật mà bạn không thể dễ dàng thực hiện với đánh giá háo hức. Nhưng phần lớn mã không cần các thủ thuật này.
cmaster

2
Đây dường như là một câu trả lời thiếu kinh nghiệm với các ngôn ngữ với sự đánh giá lười biếng. Ví dụ, những gì về cấu trúc dữ liệu vô hạn?
Andres F.

3

Như @DeadMG lưu ý Đánh giá lười biếng đòi hỏi phải giữ sổ sách. Điều này có thể tốn kém liên quan đến đánh giá háo hức. Hãy xem xét tuyên bố này:

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

Điều này sẽ mất một chút tính toán để tính toán. Nếu tôi sử dụng đánh giá lười biếng, thì tôi cần kiểm tra xem nó có được đánh giá mỗi lần tôi sử dụng không. Nếu đây là một vòng lặp được sử dụng nhiều thì chi phí tăng lên đáng kể, nhưng không có lợi ích gì.

Với đánh giá háo hức và một trình biên dịch đàng hoàng, công thức được tính toán tại thời điểm biên dịch. Hầu hết các trình tối ưu hóa sẽ chuyển nhiệm vụ ra khỏi bất kỳ vòng lặp nào xảy ra nếu thích hợp.

Đánh giá lười biếng là phù hợp nhất để tải dữ liệu sẽ được truy cập không thường xuyên và có chi phí cao để truy xuất. Do đó, nó thích hợp hơn cho các trường hợp cạnh hơn chức năng cốt lõi.

Nói chung, đó là cách thực hành tốt để đánh giá những thứ thường xuyên được truy cập càng sớm càng tốt. Đánh giá lười biếng không hoạt động với thực hành này. Nếu bạn sẽ luôn truy cập một cái gì đó, tất cả các đánh giá lười biếng sẽ làm là thêm chi phí. Chi phí / lợi ích của việc sử dụng đánh giá lười biếng giảm khi mục được truy cập trở nên ít có khả năng được truy cập hơn.

Luôn luôn sử dụng đánh giá lười biếng cũng ngụ ý tối ưu hóa sớm. Đây là một thực tiễn tồi tệ thường dẫn đến mã phức tạp và tốn kém hơn nhiều có thể là trường hợp khác. Thật không may, tối ưu hóa sớm thường dẫn đến mã thực hiện chậm hơn mã đơn giản hơn. Cho đến khi bạn có thể đo lường hiệu quả của việc tối ưu hóa, việc tối ưu hóa mã của bạn là một ý tưởng tồi.

Tránh tối ưu hóa sớm không xung đột với thực tiễn mã hóa tốt. Nếu các thực tiễn tốt không được áp dụng, tối ưu hóa ban đầu có thể bao gồm áp dụng các thực tiễn mã hóa tốt như di chuyển các tính toán ra khỏi các vòng lặp.


1
Bạn dường như đang tranh cãi về sự thiếu kinh nghiệm. Tôi đề nghị bạn đọc bài viết "Tại sao các vấn đề lập trình chức năng" của Wadler. Nó dành một phần chính giải thích lý do tại sao đánh giá lười biếng (gợi ý: nó ít liên quan đến hiệu suất, tối ưu hóa sớm hoặc "tải dữ liệu truy cập không thường xuyên" và mọi thứ phải làm với tính mô đun).
Andres F.

@AndresF Tôi đã đọc bài báo mà bạn tham khảo. Tôi đồng ý với việc sử dụng đánh giá lười biếng trong những trường hợp như vậy. Đánh giá sớm có thể không phù hợp, nhưng tôi cho rằng việc trả lại cây con cho di chuyển đã chọn có thể có lợi ích đáng kể nếu có thể thêm các di chuyển bổ sung một cách dễ dàng. Tuy nhiên, xây dựng chức năng đó có thể là tối ưu hóa sớm. Bên ngoài lập trình chức năng, tôi có các vấn đề quan trọng về hạt giống với việc sử dụng đánh giá lười biếng và việc không sử dụng đánh giá lười biếng. Có báo cáo về chi phí hiệu suất đáng kể do đánh giá lười biếng trong lập trình chức năng.
BillThor

2
Nhu la? Cũng có báo cáo về chi phí hiệu suất đáng kể khi sử dụng đánh giá háo hức (chi phí dưới dạng đánh giá không cần thiết, cũng như không chấm dứt chương trình). Có chi phí cho hầu hết các tính năng được sử dụng (mis) khác, hãy nghĩ về nó. Modularity chính nó có thể đi kèm với một chi phí; Vấn đề là nó có đáng không.
Andres F.

3

Nếu chúng ta có khả năng phải đánh giá đầy đủ một biểu thức để xác định giá trị của nó thì đánh giá lười biếng có thể là một bất lợi. Giả sử chúng tôi có một danh sách dài các giá trị boolean và chúng tôi muốn tìm hiểu xem tất cả chúng có đúng không:

[True, True, True, ... False]

Để làm điều này, chúng tôi phải xem xét mọi yếu tố trong danh sách, không có vấn đề gì, vì vậy không có khả năng lười biếng cắt đứt đánh giá. Chúng ta có thể sử dụng một nếp gấp để xác định xem tất cả các giá trị boolean trong danh sách có đúng không. Nếu chúng ta sử dụng quyền gấp, sử dụng đánh giá lười biếng, chúng ta sẽ không nhận được bất kỳ lợi ích nào của việc đánh giá lười biếng vì chúng ta phải xem xét mọi yếu tố trong danh sách:

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

Một nếp gấp bên phải sẽ chậm hơn nhiều trong trường hợp này so với nếp gấp nghiêm ngặt bên trái, không sử dụng đánh giá lười biếng:

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

Lý do là một nếp gấp nghiêm ngặt sử dụng đệ quy đuôi, có nghĩa là nó tích lũy giá trị trả về và không tích tụ và lưu trữ trong bộ nhớ một chuỗi hoạt động lớn. Điều này nhanh hơn nhiều so với quyền gấp bởi vì cả hai chức năng đều phải xem toàn bộ danh sách và quyền gấp không thể sử dụng đệ quy đuôi. Vì vậy, vấn đề là, bạn nên sử dụng bất cứ điều gì tốt nhất cho nhiệm vụ trong tay.


"Vì vậy, vấn đề là, bạn nên sử dụng bất cứ điều gì tốt nhất cho nhiệm vụ trong tay." +1
Giorgio
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.