เหตุใดแนวคิดของการประเมินแบบขี้เกียจจึงมีประโยชน์


30

ดูเหมือนว่าการประเมินนิพจน์ที่ขี้เกียจสามารถทำให้โปรแกรมเมอร์สูญเสียการควบคุมคำสั่งที่เรียกใช้งานโค้ด ฉันมีปัญหาในการทำความเข้าใจว่าทำไมจึงเป็นที่ยอมรับหรือต้องการโดยโปรแกรมเมอร์

กระบวนทัศน์นี้สามารถใช้เพื่อสร้างซอฟต์แวร์ที่สามารถคาดการณ์ได้ซึ่งทำงานได้ตามที่ตั้งใจไว้เมื่อเราไม่รับประกันว่าจะให้มีการประเมินการแสดงออกเมื่อใดและที่ไหน


10
ในกรณีส่วนใหญ่มันก็ไม่สำคัญ สำหรับคนอื่น ๆ คุณสามารถบังคับใช้ความเข้มงวดได้
Cat Plus Plus

22
จุดประสงค์ของภาษาที่ใช้งานได้จริงอย่างHaskellก็คือคุณไม่ต้องกังวลเมื่อมีการเรียกใช้โค้ดเนื่องจากไม่มีผลข้างเคียง
bitmask

21
คุณต้องหยุดคิดเกี่ยวกับ "รันโค้ด" และเริ่มคิดถึง "การคำนวณผลลัพธ์" เพราะนั่นคือสิ่งที่คุณต้องการในปัญหาที่น่าสนใจที่สุด แน่นอนว่าหลักสูตรมักจะต้องมีการโต้ตอบกับสภาพแวดล้อมด้วยวิธีใดวิธีหนึ่ง แต่นั่นอาจลดลงไปเป็นส่วนเล็ก ๆ ของรหัส สำหรับส่วนที่เหลือคุณสามารถทำงานได้อย่างหมดจดและความเกียจคร้านสามารถทำให้เหตุผลง่ายขึ้นมาก
leftaroundabout

6
คำถามในชื่อเรื่อง ("ทำไมต้องใช้การประเมินขี้เกียจ?") นั้นแตกต่างจากคำถามในร่างกาย ("คุณใช้การประเมินแบบเกียจคร้านอย่างไร") สำหรับอดีตดูคำตอบของฉันคำถามที่เกี่ยวข้องนี้
Daniel Wagner

1
ตัวอย่างเมื่อความเกียจคร้านมีประโยชน์: ใน Haskell head . sortมีO(n)ความซับซ้อนเนื่องจากความเกียจคร้าน (ไม่ใช่O(n log n)) ดูการประเมินผลขี้เกียจและเวลาซับซ้อน
Petr Pudlák

คำตอบ:


62

คำตอบจำนวนมากกำลังเข้าสู่รายการต่าง ๆ เช่นรายการที่ไม่มีที่สิ้นสุดและประสิทธิภาพที่เพิ่มขึ้นจากการคำนวณที่ไม่ได้ประเมินค่า แต่สิ่งนี้ขาดแรงจูงใจที่ยิ่งใหญ่กว่าสำหรับความขี้เกียจนั่นคือต้นแบบ

ข้อโต้แย้งคลาสสิกวางอยู่ในเอกสารที่มีการอ้างถึงมาก"Why Functional Programming Matters" (ลิงค์ PDF)โดย John Hughes ตัวอย่างคีย์ในกระดาษนั้น (ตอนที่ 5) กำลังเล่น Tic-Tac-Toe โดยใช้อัลกอริทึมการค้นหาอัลฟาเบต้า จุดสำคัญคือ (หน้า 9):

[การประเมิน Lazy] ทำให้เป็นประโยชน์ในการทำให้โมดูลเป็นโปรแกรมที่สร้างคำตอบที่เป็นไปได้จำนวนมากและตัวเลือกที่เลือกคำตอบที่เหมาะสม

โปรแกรม Tic-Tac-Toe สามารถเขียนเป็นฟังก์ชั่นที่สร้างทรีเกมทั้งหมดโดยเริ่มจากตำแหน่งที่กำหนดและฟังก์ชั่นแยกต่างหากที่ใช้งาน ณ รันไทม์สิ่งนี้ไม่ได้สร้างแผนผังเกมทั้งหมดขึ้นมาเฉพาะส่วนย่อยที่ผู้บริโภคต้องการเท่านั้น เราสามารถเปลี่ยนคำสั่งซื้อและการรวมกันซึ่งผลิตทางเลือกโดยการเปลี่ยนผู้บริโภค ไม่จำเป็นต้องเปลี่ยนเครื่องกำเนิดไฟฟ้าเลย

ในภาษาที่กระตือรือร้นคุณไม่สามารถเขียนด้วยวิธีนี้ได้เพราะคุณอาจใช้เวลามากเกินไปและความทรงจำในการสร้างต้นไม้ ดังนั้นคุณจะสิ้นสุด:

  1. การรวมการสร้างและการบริโภคเข้ากับฟังก์ชั่นเดียวกัน;
  2. การเขียนผู้ผลิตที่ทำงานได้ดีที่สุดสำหรับผู้บริโภคบางรายเท่านั้น
  3. ใช้ความเกียจคร้านของคุณเอง

กรุณาข้อมูลเพิ่มเติมหรือตัวอย่าง ฟังดูน่าสนใจ
Alex Nye

1
@AlexNye: กระดาษ John Hughes มีข้อมูลเพิ่มเติม แม้จะเป็นรายงานด้านการศึกษา - และก็ไม่ต้องสงสัยเลยว่าการข่มขู่ --- เป็นเรื่องที่เข้าถึงได้ง่ายและอ่านง่าย ถ้าไม่ใช่เพราะความยาวมันอาจจะพอดีกับคำตอบที่นี่!
Tikhon Jelvis

บางทีอาจจะเข้าใจคำตอบนี้เราต้องอ่านบทความโดยฮิวจ์ ... ไม่ทำอย่างนั้นฉันยังคงล้มเหลวที่จะดูว่าทำไมความเกียจคร้านและโมดุลจึงเกี่ยวข้องกัน
stakx

@stakx ไม่มีคำอธิบายที่ดีกว่าพวกเขาดูเหมือนจะไม่เกี่ยวข้องยกเว้นโดยบังเอิญ ข้อได้เปรียบของความเกียจคร้านในตัวอย่างนี้คือตัวสร้างแบบขี้เกียจสามารถสร้างสถานะที่เป็นไปได้ทั้งหมดของเกม แต่จะไม่เสียเวลา / หน่วยความจำทำเช่นนั้นเพราะสิ่งที่เกิดขึ้นจะถูกบริโภค เครื่องกำเนิดไฟฟ้าสามารถแยกออกจากผู้บริโภคโดยไม่ต้องเป็นเครื่องกำเนิดไฟฟ้าแบบสันหลังยาวและเป็นไปได้ (แม้ว่าจะยากขึ้น) ในการขี้เกียจโดยไม่ถูกแยกออกจากผู้บริโภค
Izkata

@ อิซทากะ: "เครื่องกำเนิดไฟฟ้าสามารถแยกออกจากผู้บริโภคได้โดยไม่ต้องเป็นเครื่องกำเนิดไฟฟ้าแบบขี้เกียจและเป็นไปได้ (แม้ว่าจะยากกว่า) ที่จะขี้เกียจโดยไม่ต้องแยกออกจากผู้บริโภค" โปรดทราบว่าเมื่อคุณไปเส้นทางนี้คุณอาจจบลงด้วยเครื่องกำเนิดไฟฟ้าที่มีความชำนาญเฉพาะทางมากเกินไป ** - เครื่องกำเนิดไฟฟ้าถูกเขียนขึ้นเพื่อเพิ่มประสิทธิภาพให้กับผู้ใช้รายหนึ่งและเมื่อนำมาใช้ใหม่กับผู้อื่น ตัวอย่างทั่วไปคือตัวแมปสัมพันธ์เชิงวัตถุที่ดึงและสร้างกราฟวัตถุทั้งหมดเพียงเพราะคุณต้องการสตริงหนึ่งจากวัตถุรูต ความเกียจคร้านหลีกเลี่ยงกรณีดังกล่าวจำนวนมาก (แต่ไม่ใช่ทั้งหมด)
sacundim

32

กระบวนทัศน์นี้สามารถใช้เพื่อสร้างซอฟต์แวร์ที่สามารถคาดการณ์ได้ซึ่งทำงานได้ตามที่ตั้งใจไว้เมื่อเราไม่รับประกันว่าจะให้มีการประเมินการแสดงออกเมื่อใดและที่ไหน

เมื่อนิพจน์ไม่มีผลข้างเคียงลำดับของนิพจน์ที่ถูกประเมินจะไม่ส่งผลกระทบต่อค่าของมันดังนั้นพฤติกรรมของโปรแกรมจะไม่ได้รับผลกระทบจากคำสั่งนั้น ดังนั้นพฤติกรรมสามารถทำนายได้อย่างสมบูรณ์แบบ

ตอนนี้ผลข้างเคียงเป็นเรื่องที่แตกต่าง หากผลข้างเคียงอาจเกิดขึ้นในลำดับใด ๆ พฤติกรรมของโปรแกรมจะไม่แน่นอนแน่นอน แต่นี่ไม่ใช่กรณี ภาษาที่ขี้เกียจเช่น Haskell ทำให้จุดนั้นมีความโปร่งใสในการอ้างอิงคือทำให้แน่ใจว่าลำดับที่นิพจน์ได้รับการประเมินจะไม่มีผลกับผลลัพธ์ของพวกเขา ใน Haskell สิ่งนี้สามารถทำได้โดยการบังคับให้การดำเนินการทั้งหมดมีผลข้างเคียงที่ผู้ใช้มองเห็นได้เกิดขึ้นภายใน IO monad สิ่งนี้ทำให้แน่ใจได้ว่าผลข้างเคียงทั้งหมดเกิดขึ้นตามลำดับที่คุณคาดหวัง


15
นี่คือเหตุผลว่าทำไมเฉพาะภาษาที่มี "ความบริสุทธิ์บังคับใช้" เช่น Haskell สนับสนุนความเกียจคร้านในทุกที่โดยค่าเริ่มต้น ภาษา "ส่งเสริมความบริสุทธิ์" เช่นสกาล่าต้องการให้โปรแกรมเมอร์พูดอย่างชัดเจนว่าพวกเขาต้องการความเกียจคร้านและขึ้นอยู่กับโปรแกรมเมอร์เพื่อให้แน่ใจว่าความเกียจคร้านจะไม่ทำให้เกิดปัญหา ภาษาที่มีความเกียจคร้านโดยค่าเริ่มต้นและมีผลข้างเคียงที่ไม่ได้ติดตามจะเป็นการยากที่จะคาดเดาโปรแกรม
Ben

1
แน่นอน monads อื่น ๆ นอกเหนือจาก IO สามารถทำให้เกิดผลข้างเคียง
jk

1
@jk เฉพาะ IO เท่านั้นที่สามารถทำให้เกิดผลข้างเคียงภายนอกได้
dave4420

@ dave4420 ใช่ แต่นั่นไม่ใช่สิ่งที่คำตอบนี้พูด
jk

1
@jk In Haskell ไม่จริง ไม่มี monad ยกเว้น IO (หรือสิ่งที่สร้างขึ้นตาม IO) มีผลข้างเคียง และนี่เป็นเพียงเพราะคอมไพเลอร์ปฏิบัติ IO แตกต่างกัน มันคิดว่า IO เป็น "ไม่เปลี่ยนรูป" Monads เป็นเพียงวิธีการที่ชาญฉลาดในการตรวจสอบลำดับการดำเนินการที่เฉพาะเจาะจง (ดังนั้นไฟล์ของคุณจะถูกลบหลังจากที่ผู้ใช้ป้อน "ใช่")
scarfridge

22

หากคุณคุ้นเคยกับฐานข้อมูลวิธีการประมวลผลข้อมูลที่พบบ่อยคือ:

  • ถามคำถามเช่น select * from foobar
  • ในขณะที่มีข้อมูลมากขึ้นให้ทำ: รับผลการค้นหาแถวถัดไปแล้วประมวลผล

คุณไม่ได้ควบคุมวิธีการสร้างผลลัพธ์และวิธีการใด (ดัชนีสแกนตารางเต็มหรือไม่) หรือเมื่อ (ควรสร้างข้อมูลทั้งหมดในครั้งเดียวหรือเพิ่มขึ้นเมื่อถูกถาม) สิ่งที่คุณรู้คือ: หากมีข้อมูลเพิ่มเติมคุณจะได้รับเมื่อคุณขอข้อมูล

การประเมินผลขี้เกียจค่อนข้างใกล้เคียงกับสิ่งเดียวกัน สมมติว่าคุณมีรายการที่ไม่สิ้นสุดกำหนดไว้เช่น ลำดับฟีโบนักชี - หากคุณต้องการตัวเลขห้าตัวคุณจะได้รับการคำนวณตัวเลขห้าตัว ถ้าคุณต้องการ 1,000 คุณจะได้ 1,000 เคล็ดลับคือรันไทม์รู้ว่าจะต้องจัดหาที่ไหนและเมื่อไหร่ มันมีประโยชน์มาก ๆ

(โปรแกรมเมอร์ Java สามารถเลียนแบบพฤติกรรมนี้ด้วย Iterators - ภาษาอื่นอาจมีบางสิ่งที่คล้ายกัน)


จุดดี. ตัวอย่างเช่นCollection2.filter()(รวมถึงวิธีการอื่น ๆ จากคลาสนั้น) ใช้การประเมินที่ขี้เกียจ: ผลลัพธ์ "ดูเหมือน" ธรรมดาCollectionแต่ลำดับของการดำเนินการอาจไม่ง่าย (หรืออย่างน้อยก็ไม่ชัดเจน) นอกจากนี้ยังมีyieldใน Python (และคุณสมบัติที่คล้ายกันใน C # ซึ่งฉันไม่จำชื่อ) ซึ่งยิ่งใกล้เคียงกับการรองรับการประเมินผลที่ขี้เกียจกว่า Iterator ปกติ
Joachim Sauer

@JoachimSauer ใน C # ผลตอบแทนของมันหรือแน่นอนคุณสามารถใช้ prer linators preq linq ประมาณครึ่งหนึ่งซึ่งขี้เกียจ
jk

+1: สำหรับการกล่าวถึงตัววนซ้ำในภาษาที่จำเป็น / เชิงวัตถุ ฉันใช้โซลูชันที่คล้ายกันสำหรับการนำสตรีมและฟังก์ชั่นสตรีมไปใช้ใน Java การใช้ตัววนซ้ำฉันสามารถใช้ฟังก์ชันเช่น take (n), dropWhile () บนอินพุตสตรีมที่ไม่ทราบความยาว
Giorgio

13

ลองถามฐานข้อมูลของคุณเพื่อหารายชื่อผู้ใช้ 2,000 คนแรกที่มีชื่อขึ้นต้นด้วย "Ab" และมีอายุมากกว่า 20 ปี พวกเขาจะต้องเป็นผู้ชาย

นี่คือแผนภาพเล็ก ๆ

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.

อย่างที่คุณเห็นจากการมีปฏิสัมพันธ์อันน่ากลัวนี้ "ฐานข้อมูล" ไม่ได้ทำอะไรเลยจนกระทั่งพร้อมที่จะรับมือกับทุกเงื่อนไข มันเป็นผลลัพธ์ที่ขี้เกียจในแต่ละขั้นตอนและใช้เงื่อนไขใหม่ทุกครั้ง

ตรงกันข้ามกับการรับผู้ใช้ 2,000 คนแรกคืนพวกเขากรองพวกเขาสำหรับ "Ab" คืนพวกเขากรองพวกเขานานกว่า 20 คืนพวกเขากลับและกรองตัวผู้และสุดท้ายก็ส่งคืนพวกเขา

ขี้เกียจโหลดสั้น ๆ


1
นี่เป็นคำอธิบายที่น่ารังเกียจจริงๆ IMHO น่าเสียดายที่ฉันมีตัวแทนไม่เพียงพอในไซต์ SE นี้เพื่อลงคะแนน จุดที่แท้จริงของการประเมินผลที่ขี้เกียจคือไม่มีผลลัพธ์ใด ๆ เหล่านี้เกิดขึ้นจริงจนกว่าสิ่งอื่นจะพร้อมที่จะบริโภค
Alnitak

คำตอบที่โพสต์ของฉันคือการพูดสิ่งเดียวกับความคิดเห็นของคุณ
sergserg

นั่นเป็นโปรแกรมประมวลผลที่สุภาพมาก
Julian

9

การประเมินค่านิพจน์ที่ขี้เกียจจะทำให้ผู้ออกแบบโค้ดที่กำหนดสูญเสียการควบคุมลำดับที่โค้ดถูกเรียกใช้

ผู้ออกแบบไม่ควรสนใจเกี่ยวกับลำดับที่นิพจน์ได้รับการประเมินหากผลลัพธ์นั้นเหมือนกัน โดยการชะลอการประเมินอาจเป็นไปได้ที่จะหลีกเลี่ยงการประเมินนิพจน์ทั้งหมดซึ่งช่วยประหยัดเวลา

คุณสามารถเห็นความคิดเดียวกันในที่ทำงานในระดับที่ต่ำกว่า: ไมโครโปรเซสเซอร์จำนวนมากสามารถดำเนินการคำสั่งที่ไม่เป็นระเบียบซึ่งช่วยให้พวกเขาใช้หน่วยปฏิบัติต่างๆได้อย่างมีประสิทธิภาพมากขึ้น กุญแจสำคัญคือพวกเขาดูการพึ่งพาระหว่างคำแนะนำและหลีกเลี่ยงการเรียงลำดับใหม่ซึ่งจะเปลี่ยนผลลัพธ์


5

มีข้อโต้แย้งหลายประการสำหรับการประเมินแบบขี้เกียจฉันคิดว่าน่าสนใจ

  1. Modularityด้วยการประเมินผลที่ขี้เกียจคุณสามารถแยกรหัสออกเป็นส่วนต่างๆ ตัวอย่างเช่นสมมติว่าคุณมีปัญหาในการ "ค้นหา 10 หลักแรกขององค์ประกอบในรายการที่มีค่าน้อยกว่า 1 ซึ่งกันและกัน" ในบางอย่างเช่น Haskell คุณสามารถเขียนได้

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

    แต่นี่เป็นเพียงภาษาที่ไม่ถูกต้องเนื่องจากถ้าคุณให้[2,3,4,5,6,7,8,9,10,11,12,0]คุณคุณจะถูกหารด้วยศูนย์ ดูคำตอบของ sacundim สำหรับสาเหตุที่สิ่งนี้ยอดเยี่ยมในทางปฏิบัติ

  2. สิ่งต่าง ๆ ทำงานได้อย่างเคร่งครัด (ตั้งใจให้ใช้) โปรแกรมจำนวนมากสิ้นสุดด้วยการประเมินที่ไม่เข้มงวดกว่าการประเมินที่เข้มงวด หากโปรแกรมของคุณสิ้นสุดด้วยกลยุทธ์การประเมิน "กระตือรือร้น" โปรแกรมจะยุติการใช้งานด้วย "ขี้เกียจ" แต่ oposite นั้นไม่เป็นความจริง คุณได้รับสิ่งต่าง ๆ เช่นโครงสร้างข้อมูลที่ไม่มีที่สิ้นสุด (ซึ่งจริงๆแล้วก็แค่ค่อนข้างเย็น) เป็นตัวอย่างเฉพาะของปรากฏการณ์นี้ โปรแกรมอื่น ๆ ใช้ภาษาขี้เกียจ

  3. Optimality การประเมินความต้องการการโทรตามความต้องการนั้นเหมาะสมที่สุดแบบ asymptotically ตามเวลา แม้ว่าภาษาสันหลังยาวที่สำคัญ (ซึ่งโดยหลักคือ Haskell และ Haskell) ไม่ได้สัญญาว่าจะโทรตามความต้องการ แต่คุณสามารถคาดหวังได้ว่าแบบจำลองราคาที่เหมาะสมที่สุด เครื่องวิเคราะห์ความเข้มงวด (และการประเมินผลเก็งกำไร) ทำให้ค่าใช้จ่ายลดลงในทางปฏิบัติ อวกาศเป็นเรื่องที่ซับซ้อนมากขึ้น

  4. บังคับให้ความบริสุทธิ์โดยใช้การประเมินที่ขี้เกียจทำให้การรับมือกับผลข้างเคียงในลักษณะที่ไม่ได้รับความเจ็บปวดโดยรวมเนื่องจากเมื่อคุณวางไว้โปรแกรมเมอร์ก็สูญเสียการควบคุม นี่เป็นสิ่งที่ดี ความโปร่งใสอ้างอิงทำให้การเขียนโปรแกรมการหักเหและเหตุผลเกี่ยวกับโปรแกรมง่ายขึ้นมาก ภาษาที่เข้มงวดอย่างหลีกเลี่ยงไม่ได้กับความกดดันของการมีบิตที่ไม่บริสุทธิ์ - สิ่งที่ Haskell และ Clean ได้ต่อต้านอย่างสวยงาม นี่ไม่ได้เป็นการบอกว่าผลข้างเคียงเป็นสิ่งชั่วร้ายเสมอไป แต่การควบคุมพวกมันมีประโยชน์มากด้วยเหตุผลนี้เพียงอย่างเดียวก็เพียงพอที่จะใช้ภาษาที่ขี้เกียจ


2

สมมติว่าคุณมีข้อเสนอการคำนวณราคาแพงจำนวนมาก แต่ไม่ทราบว่าจะต้องใช้การคำนวณแบบใดหรือแบบใด คุณสามารถเพิ่มโปรโตคอล Mother-May-i ที่ซับซ้อนเพื่อบังคับให้ผู้บริโภคเข้าใจว่ามีอะไรให้บ้างและทริกเกอร์การคำนวณที่ยังไม่เสร็จ หรือคุณสามารถให้อินเทอร์เฟซที่ทำหน้าที่เหมือนกับการคำนวณเสร็จสิ้นทั้งหมด

นอกจากนี้สมมติว่าคุณมีผลลัพธ์ที่ไม่สิ้นสุด ชุดของช่วงเวลาทั้งหมดเช่น เห็นได้ชัดว่าคุณไม่สามารถคำนวณชุดล่วงหน้าได้ดังนั้นการดำเนินการใด ๆ ในโดเมนของช่วงเวลาจำเป็นต้องขี้เกียจ


1

ด้วยการประเมินผลที่ขี้เกียจคุณไม่สูญเสียการควบคุมเกี่ยวกับการเรียกใช้โค้ดมันยังคงถูกกำหนดไว้อย่างแน่นอน มันยากที่จะใช้กับมันแม้ว่า

การประเมินสันหลังยาวมีประโยชน์เพราะเป็นวิธีการลดระยะแลมบ์ดาซึ่งจะยุติในบางกรณีที่การประเมินความกระตือรือร้นจะล้มเหลว แต่ไม่ได้กลับกัน ซึ่งรวมถึง 1) เมื่อคุณต้องการเชื่อมโยงไปยังผลลัพธ์การคำนวณก่อนที่คุณจะดำเนินการคำนวณจริงตัวอย่างเช่นเมื่อคุณสร้างโครงสร้างกราฟ cyclic แต่ต้องการที่จะทำในรูปแบบการทำงาน 2) เมื่อคุณกำหนดโครงสร้างข้อมูลที่ไม่มีที่สิ้นสุด เพื่อใช้เพียงส่วนหนึ่งของโครงสร้างข้อมูล

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.