ฉันสงสัยมานานแล้วว่าทำไมการประเมินแบบขี้เกียจจึงมีประโยชน์ ฉันยังไม่เคยมีใครอธิบายให้ฉันเข้าใจได้ ส่วนใหญ่จะจบลงด้วยการ "เชื่อใจฉัน"
หมายเหตุ: ฉันไม่ได้หมายถึงการท่องจำ
ฉันสงสัยมานานแล้วว่าทำไมการประเมินแบบขี้เกียจจึงมีประโยชน์ ฉันยังไม่เคยมีใครอธิบายให้ฉันเข้าใจได้ ส่วนใหญ่จะจบลงด้วยการ "เชื่อใจฉัน"
หมายเหตุ: ฉันไม่ได้หมายถึงการท่องจำ
คำตอบ:
ส่วนใหญ่เป็นเพราะสามารถมีประสิทธิภาพมากกว่า - ไม่จำเป็นต้องคำนวณค่าหากจะไม่ใช้ ตัวอย่างเช่นฉันอาจส่งค่าสามค่าไปยังฟังก์ชัน แต่ขึ้นอยู่กับลำดับของนิพจน์เงื่อนไขอาจใช้เฉพาะส่วนย่อยเท่านั้น ในภาษาเช่น C ค่าทั้งสามจะถูกคำนวณอยู่ดี แต่ใน Haskell จะคำนวณเฉพาะค่าที่จำเป็นเท่านั้น
นอกจากนี้ยังช่วยให้มีสิ่งดีๆเช่นรายการที่ไม่มีที่สิ้นสุด ฉันไม่มีรายการที่ไม่มีที่สิ้นสุดในภาษาเช่น C แต่ใน Haskell นั้นไม่มีปัญหา รายการที่ไม่มีที่สิ้นสุดมักใช้บ่อยในบางพื้นที่ของคณิตศาสตร์ดังนั้นจึงมีประโยชน์ที่จะมีความสามารถในการจัดการ
ตัวอย่างที่เป็นประโยชน์ของการประเมินแบบขี้เกียจคือการใช้quickSort
:
quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)
หากตอนนี้เราต้องการค้นหาขั้นต่ำของรายการเราสามารถกำหนดได้
minimum ls = head (quickSort ls)
ซึ่งจะเรียงลำดับรายการก่อนแล้วจึงนำองค์ประกอบแรกของรายการ อย่างไรก็ตามเนื่องจากขี้เกียจการประเมินจึงมีเพียงส่วนหัวเท่านั้นที่คำนวณได้ ตัวอย่างเช่นถ้าเราใช้ขั้นต่ำของรายการ[2, 1, 3,]
QuickSort จะกรององค์ประกอบทั้งหมดที่มีขนาดเล็กกว่าสองรายการออกก่อน จากนั้นก็ทำการ QuickSort ในสิ่งนั้น (คืนรายการซิงเกิล [1]) ซึ่งเพียงพอแล้ว เนื่องจากขี้เกียจประเมินผลที่เหลือจึงไม่ถูกจัดเรียงช่วยประหยัดเวลาในการคำนวณได้มาก
แน่นอนว่านี่เป็นตัวอย่างที่ง่ายมาก แต่ความเกียจคร้านทำงานในลักษณะเดียวกันกับโปรแกรมที่มีขนาดใหญ่มาก
อย่างไรก็ตามมีข้อเสียทั้งหมดนี้: การคาดการณ์ความเร็วรันไทม์และการใช้หน่วยความจำของโปรแกรมของคุณยากขึ้น นี่ไม่ได้หมายความว่าโปรแกรมขี้เกียจทำงานช้าลงหรือใช้หน่วยความจำมากขึ้น แต่ควรรู้ไว้
take k $ quicksort list
จะใช้เวลาเพียง O (n + บันทึก k k) n = length list
เวลาที่ ด้วยการเปรียบเทียบแบบไม่ขี้เกียจสิ่งนี้จะใช้เวลา O (n log n) เสมอ
ฉันพบว่าการประเมินแบบขี้เกียจมีประโยชน์สำหรับหลาย ๆ อย่าง
ประการแรกภาษาขี้เกียจที่มีอยู่ทั้งหมดเป็นภาษาที่บริสุทธิ์เนื่องจากเป็นเรื่องยากมากที่จะให้เหตุผลเกี่ยวกับผลข้างเคียงในภาษาขี้เกียจ
ภาษาบริสุทธิ์ช่วยให้คุณสามารถหาเหตุผลเกี่ยวกับคำจำกัดความของฟังก์ชันโดยใช้การให้เหตุผลเชิงเท่าเทียมกัน
foo x = x + 3
น่าเสียดายในการตั้งค่าที่ไม่เกียจคร้านข้อความจำนวนมากไม่สามารถส่งคืนได้มากกว่าการตั้งค่าแบบขี้เกียจดังนั้นจึงมีประโยชน์น้อยกว่าในภาษาเช่น ML แต่ในภาษาขี้เกียจคุณสามารถหาเหตุผลเกี่ยวกับความเท่าเทียมกันได้อย่างปลอดภัย
ประการที่สองหลายสิ่งเช่น 'ข้อ จำกัด ค่า' ใน ML ไม่จำเป็นต้องใช้ในภาษาขี้เกียจเช่น Haskell สิ่งนี้นำไปสู่การลดทอนไวยากรณ์อย่างมาก ML เช่นภาษาต้องใช้คำหลักเช่น var หรือ fun ใน Haskell สิ่งเหล่านี้พังทลายลงเหลือเพียงแนวคิดเดียว
ประการที่สามความเกียจคร้านช่วยให้คุณเขียนโค้ดที่ใช้งานได้ดีซึ่งสามารถเข้าใจได้เป็นชิ้น ๆ ใน Haskell เป็นเรื่องปกติที่จะเขียนเนื้อหาของฟังก์ชันเช่น:
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 = ...
วิธีนี้ช่วยให้คุณทำงาน 'จากบนลงล่าง' ได้แม้ว่าจะเข้าใจเนื้อหาของฟังก์ชัน ภาษาที่คล้าย ML บังคับให้คุณใช้ภาษาlet
ที่ได้รับการประเมินอย่างเคร่งครัด ด้วยเหตุนี้คุณจึงไม่กล้าที่จะ "ยก" ประโยคคำสั่งให้ออกไปที่เนื้อหาหลักของฟังก์ชันเพราะถ้าราคาแพง (หรือมีผลข้างเคียง) คุณไม่ต้องการให้มีการประเมินเสมอไป Haskell สามารถ 'ผลักดัน' รายละเอียดไปยังส่วนที่ชัดเจนเพราะรู้ว่าเนื้อหาของประโยคนั้นจะได้รับการประเมินตามที่จำเป็นเท่านั้น
ในทางปฏิบัติเรามักจะใช้ยามและยุบมันเพิ่มเติมเพื่อ:
foo x y
| condition1 = some (complicated set of combinators) (involving bigscaryexpression)
| condition2 = bigscaryexpression
| otherwise = Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
ประการที่สี่ความเกียจคร้านบางครั้งมีการแสดงออกที่สวยงามกว่าของอัลกอริทึมบางอย่าง 'การจัดเรียงอย่างรวดเร็ว' ที่ขี้เกียจใน Haskell เป็นแบบซับเดียวและมีประโยชน์ที่ว่าหากคุณดูเพียงไม่กี่รายการแรกคุณจะจ่ายเฉพาะต้นทุนตามสัดส่วนของค่าใช้จ่ายในการเลือกรายการเหล่านั้นเท่านั้น ไม่มีสิ่งใดป้องกันไม่ให้คุณทำสิ่งนี้อย่างเคร่งครัด แต่คุณอาจต้องเข้ารหัสอัลกอริทึมใหม่ทุกครั้งเพื่อให้ได้ประสิทธิภาพที่ไม่แสดงอาการเหมือนกัน
ประการที่ห้าความเกียจคร้านช่วยให้คุณกำหนดโครงสร้างการควบคุมใหม่ในภาษาได้ คุณไม่สามารถเขียน 'if .. then .. else .. ' ใหม่เหมือนสร้างด้วยภาษาที่เข้มงวด หากคุณพยายามกำหนดฟังก์ชันเช่น:
if' True x y = x
if' False x y = y
ในภาษาที่เข้มงวดจากนั้นทั้งสองสาขาจะได้รับการประเมินโดยไม่คำนึงถึงค่าเงื่อนไข มันแย่ลงเมื่อคุณพิจารณาลูป โซลูชันที่เข้มงวดทั้งหมดต้องใช้ภาษาในการเสนอใบเสนอราคาหรือโครงสร้างแลมบ์ดาที่ชัดเจน
ในที่สุดในหลอดเลือดดำเดียวกันกลไกที่ดีที่สุดในการจัดการกับผลข้างเคียงในระบบประเภทเช่น monads สามารถแสดงออกได้อย่างมีประสิทธิภาพในสภาพแวดล้อมที่ขี้เกียจเท่านั้น สิ่งนี้สามารถเห็นได้จากการเปรียบเทียบความซับซ้อนของเวิร์กโฟลว์ของ F # กับ Haskell Monads (คุณสามารถกำหนด monad ในภาษาที่เข้มงวดได้ แต่น่าเสียดายที่คุณมักจะทำผิดกฎหมาย monad หรือสองข้อเนื่องจากไม่มีความเกียจคร้านและขั้นตอนการทำงานโดยการเปรียบเทียบรับสัมภาระที่เข้มงวดจำนวนมาก)
let
เป็นสัตว์ร้ายที่อันตรายในรูปแบบ R6RS จะทำให้สุ่ม#f
ปรากฏในคำของคุณได้ทุกที่ที่ผูกปมอย่างเคร่งครัดนำไปสู่วงจร! ไม่มีจุดมุ่งหมายในการเล่นสำนวน แต่let
การผูกแบบเรียกซ้ำอย่างเคร่งครัดนั้นสมเหตุสมผลในภาษาที่เกียจคร้าน ความเข้มงวดยังทำให้ความจริงที่ว่าwhere
ไม่มีวิธีใดที่จะสั่งให้เกิดผลกระทบที่สัมพันธ์กันได้เลยยกเว้นโดย SCC เป็นการสร้างระดับคำสั่งผลของมันอาจเกิดขึ้นในลำดับใดก็ได้อย่างเคร่งครัดและแม้ว่าคุณจะมีภาษาที่บริสุทธิ์ก็ตาม#f
ปัญหา. ไขwhere
ปริศนารหัสของคุณด้วยความกังวลที่ไม่เกี่ยวข้องกับท้องถิ่น
ifFunc(True, x, y)
เป็นไปได้ในการประเมินทั้งสองx
และแทนเพียงy
x
มีความแตกต่างระหว่างการประเมินคำสั่งซื้อปกติกับการประเมินแบบขี้เกียจ (เช่นเดียวกับใน Haskell)
square x = x * x
การประเมินนิพจน์ต่อไปนี้ ...
square (square (square 2))
... ด้วยการประเมินอย่างกระตือรือร้น:
> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256
... ด้วยการประเมินคำสั่งซื้อปกติ:
> (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
... ด้วยความขี้เกียจประเมิน:
> (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
นั่นเป็นเพราะการประเมินแบบขี้เกียจมองไปที่โครงสร้างไวยากรณ์และทำการแปลงแบบต้นไม้ ...
square (square (square 2))
||
\/
*
/ \
\ /
square (square 2)
||
\/
*
/ \
\ /
*
/ \
\ /
square 2
||
\/
*
/ \
\ /
*
/ \
\ /
*
/ \
\ /
2
... ในขณะที่การประเมินคำสั่งซื้อปกติจะขยายข้อความเท่านั้น
นั่นเป็นเหตุผลที่เมื่อใช้การประเมินแบบขี้เกียจเราจะมีประสิทธิภาพมากขึ้น (การประเมินจะยุติบ่อยกว่ากลยุทธ์อื่น ๆ ) ในขณะที่ประสิทธิภาพเทียบเท่ากับการประเมินอย่างกระตือรือร้น (อย่างน้อยก็ใน O-notation)
การประเมินความเกียจคร้านที่เกี่ยวข้องกับ CPU ในลักษณะเดียวกับการรวบรวมขยะที่เกี่ยวข้องกับ RAM GC ช่วยให้คุณสามารถแสร้งทำเป็นว่าคุณมีหน่วยความจำไม่ จำกัด จำนวนและขอวัตถุในหน่วยความจำได้มากเท่าที่คุณต้องการ รันไทม์จะเรียกคืนวัตถุที่ใช้ไม่ได้โดยอัตโนมัติ LE ช่วยให้คุณแสร้งทำเป็นว่าคุณมีทรัพยากรการคำนวณไม่ จำกัด - คุณสามารถคำนวณได้มากเท่าที่คุณต้องการ รันไทม์จะไม่ดำเนินการคำนวณที่ไม่จำเป็น (สำหรับกรณีที่กำหนด)
อะไรคือข้อได้เปรียบในทางปฏิบัติของโมเดล "แสร้งทำเป็น" เหล่านี้? มันปล่อยนักพัฒนา (ในระดับหนึ่ง) จากการจัดการทรัพยากรและลบโค้ดสำเร็จรูปบางส่วนออกจากแหล่งที่มาของคุณ แต่สิ่งที่สำคัญกว่านั้นคือคุณสามารถนำโซลูชันของคุณกลับมาใช้ใหม่ได้อย่างมีประสิทธิภาพในบริบทที่กว้างขึ้น
ลองนึกภาพว่าคุณมีรายการของตัวเลข S และหมายเลข N คุณต้องหาหมายเลขที่ใกล้เคียงที่สุดกับหมายเลข N หมายเลข M จากรายการ S คุณสามารถมีสองบริบท: N เดียวและรายการ L ของ Ns บางรายการ (ei สำหรับแต่ละ N ใน L คุณค้นหา M ที่ใกล้ที่สุดใน S) หากคุณใช้การประเมินแบบขี้เกียจคุณสามารถจัดเรียง S และใช้การค้นหาแบบไบนารีเพื่อค้นหา M ถึง N ที่ใกล้เคียงที่สุดสำหรับการเรียงลำดับแบบขี้เกียจที่ดีจะต้องใช้ขั้นตอน O (ขนาด (S)) สำหรับ N และ O เดียว (ln (ขนาด (S)) * (size (S) + size (L))) ขั้นตอนสำหรับ L ที่กระจายอย่างเท่าเทียมกันหากคุณไม่มีการประเมินแบบขี้เกียจเพื่อให้ได้ประสิทธิภาพสูงสุดคุณต้องใช้อัลกอริทึมสำหรับแต่ละบริบท
หากคุณเชื่อว่าไซมอนเพย์ตันโจนส์และการประเมินผลขี้เกียจไม่สำคัญต่อ seแต่เป็น 'เสื้อผม' ที่บังคับให้นักออกแบบเพื่อให้ภาษาที่บริสุทธิ์ ฉันพบว่าตัวเองรู้สึกเห็นอกเห็นใจกับมุมมองนี้
Richard Bird, John Hughes และส่วนขยายที่น้อยกว่านั้น Ralf Hinze สามารถทำสิ่งที่น่าอัศจรรย์ด้วยการประเมินที่ขี้เกียจ การอ่านงานของพวกเขาจะช่วยให้คุณรู้สึกซาบซึ้ง ดีจุดเริ่มต้นที่มีซูโดกุของนกที่งดงามแก้และกระดาษฮิวจ์สในเรื่องการเขียนโปรแกรมทำไมฟังก์ชั่น
IO
monad) ลายเซ็นmain
จะเป็นString -> String
และคุณสามารถเขียนโปรแกรมโต้ตอบได้อย่างถูกต้อง
IO
monad?
พิจารณาโปรแกรม tic-tac-toe มีสี่ฟังก์ชั่น:
สิ่งนี้ทำให้เกิดการแยกความกังวลอย่างชัดเจน โดยเฉพาะอย่างยิ่งฟังก์ชั่นการเคลื่อนย้ายและฟังก์ชั่นการประเมินบอร์ดเป็นฟังก์ชันเดียวที่ต้องเข้าใจกฎของเกม: ฟังก์ชันย้ายทรีและฟังก์ชันย่อส่วนสามารถใช้ซ้ำได้อย่างสมบูรณ์
ตอนนี้ให้ลองใช้หมากรุกแทน tic-tac-toe ในภาษา "กระตือรือร้น" (เช่นทั่วไป) สิ่งนี้จะใช้ไม่ได้เพราะแผนผังการย้ายจะไม่พอดีกับหน่วยความจำ ดังนั้นตอนนี้การประเมินบอร์ดและฟังก์ชันการสร้างการเคลื่อนย้ายจึงจำเป็นต้องผสมกับทรีการย้ายและตรรกะขั้นต่ำเนื่องจากต้องใช้ตรรกะขั้นต่ำเพื่อตัดสินใจว่าจะสร้างการเคลื่อนไหวใด โครงสร้างโมดูลาร์ที่สะอาดสวยงามของเราหายไป
อย่างไรก็ตามในภาษาที่ขี้เกียจองค์ประกอบของทรีการเคลื่อนไหวจะถูกสร้างขึ้นเพื่อตอบสนองความต้องการจากฟังก์ชันขั้นต่ำเท่านั้น: ไม่จำเป็นต้องสร้างแผนผังการย้ายทั้งหมดก่อนที่เราจะปล่อยให้ minimax หลวมบนองค์ประกอบด้านบน ดังนั้นโครงสร้างโมดูลาร์ที่สะอาดของเราจึงยังคงใช้งานได้ในเกมจริง
นี่คืออีกสองประเด็นที่ฉันไม่เชื่อว่าจะถูกนำมาอภิปราย
ความเกียจคร้านเป็นกลไกการประสานในสภาพแวดล้อมที่เกิดขึ้นพร้อมกัน เป็นวิธีที่ง่ายและมีน้ำหนักเบาในการสร้างการอ้างอิงไปยังการคำนวณบางอย่างและแบ่งปันผลลัพธ์ระหว่างเธรดต่างๆ หากเธรดหลายเธรดพยายามเข้าถึงค่าที่ไม่ได้ประเมินจะมีเพียงเธรดเดียวเท่านั้นที่จะดำเนินการและเธรดอื่น ๆ จะบล็อกตามนั้นโดยได้รับค่าเมื่อพร้อมใช้งาน
ความเกียจคร้านเป็นพื้นฐานในการตัดจำหน่ายโครงสร้างข้อมูลในสภาพแวดล้อมที่บริสุทธิ์ Okasaki อธิบายไว้ในรายละเอียดโครงสร้างข้อมูลที่ใช้งานได้อย่างหมดจดแต่แนวคิดพื้นฐานก็คือการประเมินแบบขี้เกียจเป็นรูปแบบการกลายพันธุ์ที่ควบคุมได้ซึ่งมีความสำคัญต่อการอนุญาตให้เราใช้โครงสร้างข้อมูลบางประเภทได้อย่างมีประสิทธิภาพ ในขณะที่เรามักพูดถึงความขี้เกียจที่บังคับให้เราสวมเสื้อคลุมผมที่บริสุทธิ์ แต่อีกวิธีหนึ่งก็ใช้ได้เช่นกันนั่นคือคุณสมบัติทางภาษาที่เสริมการทำงานร่วมกัน
เมื่อคุณเปิดคอมพิวเตอร์และ Windows ละเว้นจากการเปิดทุกไดเร็กทอรีเดียวบนฮาร์ดไดรฟ์ของคุณใน Windows Explorer และละเว้นจากการเปิดทุกโปรแกรมที่ติดตั้งบนคอมพิวเตอร์ของคุณจนกว่าคุณจะระบุว่าจำเป็นต้องมีไดเร็กทอรีบางรายการหรือจำเป็นต้องมีโปรแกรมบางอย่าง คือการประเมินแบบ "ขี้เกียจ"
การประเมินแบบ "ขี้เกียจ" กำลังดำเนินการเมื่อและตามที่จำเป็น จะมีประโยชน์เมื่อเป็นคุณลักษณะของภาษาโปรแกรมหรือไลบรารีเพราะโดยทั่วไปแล้วการประเมินแบบขี้เกียจด้วยตัวคุณเองจะทำได้ยากกว่าการคำนวณล่วงหน้าทุกอย่าง
พิจารณาสิ่งนี้:
if (conditionOne && conditionTwo) {
doSomething();
}
วิธี doSomething () จะถูกดำเนินการก็ต่อเมื่อ conditionOne เป็นจริงและ conditionTwo เป็นจริง ในกรณีที่ conditionOne เป็นเท็จทำไมคุณต้องคำนวณผลลัพธ์ของ conditionTwo การประเมิน conditionTwo จะเสียเวลาในกรณีนี้โดยเฉพาะอย่างยิ่งถ้าเงื่อนไขของคุณเป็นผลมาจากกระบวนการวิธีการบางอย่าง
นั่นคือตัวอย่างหนึ่งของความสนใจในการประเมินที่ขี้เกียจ ...
สามารถเพิ่มประสิทธิภาพ นี่คือสิ่งที่ดูชัดเจน แต่จริงๆแล้วมันไม่สำคัญที่สุด (โปรดทราบว่าความเกียจคร้านสามารถฆ่าประสิทธิภาพได้เช่นกัน - ข้อเท็จจริงนี้ไม่ชัดเจนในทันทีอย่างไรก็ตามการจัดเก็บผลลัพธ์ชั่วคราวจำนวนมากแทนที่จะคำนวณทันทีคุณจะสามารถใช้ RAM ได้มากพอสมควร)
ช่วยให้คุณกำหนดโครงสร้างการควบคุมโฟลว์ในโค้ดระดับผู้ใช้ปกติแทนที่จะเป็นฮาร์ดโค้ดในภาษา (เช่น Java มีfor
ลูป Haskell มีfor
ฟังก์ชัน Java มีการจัดการข้อยกเว้น Haskell มี monad ข้อยกเว้นหลายประเภท C # มีgoto
; Haskell มี monad ต่อเนื่อง ... )
ช่วยให้คุณแยกอัลกอริทึมในการสร้างข้อมูลจากอัลกอริทึมเพื่อตัดสินใจว่าจะสร้างข้อมูลเท่าใด คุณสามารถเขียนฟังก์ชันหนึ่งที่สร้างรายการผลลัพธ์ที่ไม่มีที่สิ้นสุดตามแนวคิดและอีกฟังก์ชันหนึ่งที่ประมวลผลรายการนี้ได้มากเท่าที่จะตัดสินใจได้ ยิ่งไปกว่านั้นคุณสามารถมีฟังก์ชันเครื่องกำเนิดไฟฟ้าห้าฟังก์ชันและฟังก์ชันสำหรับผู้บริโภคห้าฟังก์ชันและคุณสามารถสร้างชุดค่าผสมใด ๆ ได้อย่างมีประสิทธิภาพแทนที่จะเขียนโค้ดด้วยตนเอง 5 x 5 = 25 ฟังก์ชันที่รวมการกระทำทั้งสองอย่างพร้อมกัน (!) เราทุกคนรู้ดีว่าการแยกส่วนเป็นสิ่งที่ดี
มันมากหรือน้อยกว่าที่คุณกองกำลังในการออกแบบบริสุทธิ์ภาษาทำงาน การใช้ทางลัดมักเป็นเรื่องที่น่าดึงดูดอยู่เสมอ แต่ในภาษาที่ขี้เกียจความไม่บริสุทธิ์ที่น้อยที่สุดทำให้รหัสของคุณไม่สามารถคาดเดาได้อย่างรุนแรงซึ่งเป็นการต่อต้านการใช้ทางลัดอย่างมาก
ประโยชน์อย่างมากอย่างหนึ่งของความเกียจคร้านคือความสามารถในการเขียนโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปโดยมีขอบเขตการตัดจำหน่ายที่สมเหตุสมผล ตัวอย่างง่ายๆคือสแต็กที่ไม่เปลี่ยนรูป (ใช้ 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)
รหัสมีความสมเหตุสมผล แต่การต่อท้ายสองสแต็ก x และ y จะใช้เวลา O (ความยาว x) ในกรณีที่ดีที่สุดแย่ที่สุดและโดยเฉลี่ย การต่อท้ายสองสแต็กเป็นการดำเนินการแบบเสาหินโดยจะสัมผัสกับโหนดทั้งหมดในสแต็ก x
เราสามารถเขียนโครงสร้างข้อมูลใหม่เป็น lazy stack ได้:
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
ทำงานโดยการระงับการประเมินโค้ดในตัวสร้าง เมื่อประเมินโดยใช้ค่าส่งกลับถูกแคชและนำกลับมาใช้ในทุกที่ตามมา.Force()
.Force()
สำหรับเวอร์ชัน lazy การต่อท้ายเป็นการดำเนินการ O (1): ส่งคืน 1 โหนดและระงับการสร้างรายการใหม่ เมื่อคุณได้รับส่วนหัวของรายการนี้ระบบจะประเมินเนื้อหาของโหนดบังคับให้ส่งคืนส่วนหัวและสร้างการระงับหนึ่งรายการด้วยองค์ประกอบที่เหลือดังนั้นการรับส่วนหัวของรายการจึงเป็นการดำเนินการ O (1)
ดังนั้นรายการขี้เกียจของเราจึงอยู่ในสถานะที่คงที่ในการสร้างใหม่คุณไม่ต้องจ่ายค่าใช้จ่ายในการสร้างรายการนี้ขึ้นใหม่จนกว่าคุณจะสำรวจองค์ประกอบทั้งหมด ด้วยความเกียจคร้านรายการนี้รองรับ O (1) consing และต่อท้าย ที่น่าสนใจคือเนื่องจากเราไม่ได้ประเมินโหนดจนกว่าจะมีการเข้าถึงจึงเป็นไปได้ทั้งหมดที่จะสร้างรายการที่มีองค์ประกอบที่อาจไม่มีที่สิ้นสุด
โครงสร้างข้อมูลด้านบนไม่จำเป็นต้องมีการคำนวณโหนดใหม่ในการส่งผ่านแต่ละครั้งดังนั้นจึงแตกต่างจาก vanilla IEnumerables ใน. NET อย่างชัดเจน
ตัวอย่างข้อมูลนี้แสดงความแตกต่างระหว่างการประเมินแบบขี้เกียจและไม่ขี้เกียจ แน่นอนว่าฟังก์ชัน fibonacci นี้สามารถปรับให้เหมาะสมได้เองและใช้การประเมินแบบขี้เกียจแทนการเรียกซ้ำ แต่จะทำให้เสียตัวอย่าง
สมมติว่าเราอาจต้องใช้ตัวเลข 20 ตัวแรกเพื่ออะไรบางอย่างโดยที่ไม่ใช่การประเมินแบบขี้เกียจต้องสร้างตัวเลขทั้งหมด 20 ตัวล่วงหน้า แต่ด้วยการประเมินที่ขี้เกียจพวกเขาจะถูกสร้างขึ้นตามความจำเป็นเท่านั้น ดังนั้นคุณจะจ่ายเฉพาะราคาคำนวณเมื่อจำเป็น
เอาต์พุตตัวอย่าง
ไม่ขี้เกียจรุ่น: 0.023373 รุ่นขี้เกียจ: 0.000009 ไม่ขี้เกียจเอาต์พุต: 0.000921 เอาต์พุตขี้เกียจ: 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)
การประเมินความเกียจคร้านมีประโยชน์ที่สุดกับโครงสร้างข้อมูล คุณสามารถกำหนดอาร์เรย์หรือเวกเตอร์โดยอุปนัยระบุเฉพาะบางจุดในโครงสร้างและแสดงอื่น ๆ ทั้งหมดในรูปของอาร์เรย์ทั้งหมด สิ่งนี้ช่วยให้คุณสร้างโครงสร้างข้อมูลได้อย่างรัดกุมและมีประสิทธิภาพรันไทม์สูง
เพื่อดูนี้ในการดำเนินการที่คุณสามารถดูได้ที่ห้องสมุดเครือข่ายของฉันประสาทที่เรียกว่าสัญชาตญาณ ใช้ประโยชน์จากการประเมินอย่างขี้เกียจเพื่อความสง่างามและประสิทธิภาพสูง ตัวอย่างเช่นฉันกำจัดการคำนวณการเปิดใช้งานที่จำเป็นแบบดั้งเดิมโดยสิ้นเชิง การแสดงออกที่เรียบง่ายขี้เกียจทำทุกอย่างเพื่อฉัน
ตัวอย่างนี้ใช้ในฟังก์ชันการเปิดใช้งานและในอัลกอริทึมการเรียนรู้ backpropagation (ฉันสามารถโพสต์ลิงก์ได้เพียงสองลิงก์ดังนั้นคุณจะต้องค้นหาlearnPat
ฟังก์ชันในAI.Instinct.Train.Delta
โมดูลด้วยตัวเอง) ตามเนื้อผ้าทั้งสองต้องการอัลกอริทึมซ้ำที่ซับซ้อนกว่านี้มาก
คนอื่นให้เหตุผลใหญ่ ๆ ไปหมดแล้ว แต่ฉันคิดว่าแบบฝึกหัดที่มีประโยชน์ที่จะช่วยให้เข้าใจว่าทำไมความขี้เกียจจึงสำคัญคือการพยายามเขียนฟังก์ชันจุดตายตัวด้วยภาษาที่เข้มงวด
ใน Haskell ฟังก์ชันจุดคงที่นั้นง่ายมาก:
fix f = f (fix f)
สิ่งนี้ขยายเป็น
f (f (f ....
แต่เนื่องจาก Haskell ขี้เกียจห่วงโซ่การคำนวณที่ไม่มีที่สิ้นสุดจึงไม่มีปัญหา การประเมินเสร็จสิ้น "จากภายนอกสู่ภายใน" และทุกอย่างทำงานได้อย่างยอดเยี่ยม:
fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)
ที่สำคัญไม่สำคัญว่าfix
ขี้เกียจ แต่f
ขี้เกียจ เมื่อคุณได้รับการเข้มงวดf
แล้วคุณสามารถโยนมือของคุณขึ้นไปในอากาศและยอมแพ้หรือขยายขนาดและทำให้ยุ่งเหยิง (นี่เหมือนกับสิ่งที่โนอาห์พูดเกี่ยวกับการเป็นห้องสมุดที่เข้มงวด / ขี้เกียจไม่ใช่ภาษา)
ลองนึกภาพการเขียนฟังก์ชันเดียวกันใน Scala ที่เข้มงวด:
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)
}
แน่นอนคุณจะได้รับสแต็คล้น หากคุณต้องการให้มันทำงานคุณต้องทำให้f
อาร์กิวเมนต์ call-by-need:
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)
ฉันไม่รู้ว่าตอนนี้คุณคิดยังไง แต่ฉันคิดว่าการคิดว่าการประเมินแบบขี้เกียจเป็นปัญหาของห้องสมุดนั้นมีประโยชน์มากกว่าฟีเจอร์ด้านภาษา
ฉันหมายความว่าในภาษาที่เข้มงวดฉันสามารถใช้การประเมินผลแบบขี้เกียจได้โดยการสร้างโครงสร้างข้อมูลบางอย่างและในภาษาขี้เกียจ (อย่างน้อยก็ Haskell) ฉันสามารถขอความเข้มงวดได้เมื่อฉันต้องการ ดังนั้นตัวเลือกภาษาไม่ได้ทำให้โปรแกรมของคุณขี้เกียจหรือไม่ขี้เกียจ แต่มีผลกับสิ่งที่คุณได้รับโดยค่าเริ่มต้น
เมื่อคุณคิดเช่นนั้นแล้วให้นึกถึงสถานที่ทั้งหมดที่คุณเขียนโครงสร้างข้อมูลที่คุณสามารถใช้ในการสร้างข้อมูลในภายหลัง (โดยไม่ได้ดูมากเกินไปก่อนหน้านั้น) และคุณจะเห็นการใช้งานมากมายสำหรับคนขี้เกียจ การประเมินผล
การใช้ประโยชน์จากการประเมินแบบขี้เกียจที่มีประโยชน์ที่สุดที่ฉันเคยใช้คือฟังก์ชันที่เรียกชุดของฟังก์ชันย่อยตามลำดับเฉพาะ หากฟังก์ชันย่อยใด ๆ เหล่านี้ล้มเหลว (ส่งคืนเป็นเท็จ) ฟังก์ชันการโทรจำเป็นต้องส่งคืนทันที ดังนั้นฉันสามารถทำได้ด้วยวิธีนี้:
bool Function(void) {
if (!SubFunction1())
return false;
if (!SubFunction2())
return false;
if (!SubFunction3())
return false;
(etc)
return true;
}
หรือวิธีแก้ปัญหาที่หรูหรากว่า:
bool Function(void) {
if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
return false;
return true;
}
เมื่อคุณเริ่มใช้งานคุณจะเห็นโอกาสในการใช้งานบ่อยขึ้นเรื่อย ๆ
หากไม่มีการประเมินที่ขี้เกียจคุณจะไม่ได้รับอนุญาตให้เขียนสิ่งนี้:
if( obj != null && obj.Value == correctValue )
{
// do smth
}
เหนือสิ่งอื่นใดภาษาขี้เกียจอนุญาตให้มีโครงสร้างข้อมูลที่ไม่มีที่สิ้นสุดหลายมิติ
ในขณะที่โครงร่าง python และอื่น ๆ อนุญาตให้มีโครงสร้างข้อมูลที่ไม่มีที่สิ้นสุดของมิติเดียวพร้อมสตรีมคุณสามารถสำรวจไปตามมิติเดียวเท่านั้น
ความเกียจคร้านมีประโยชน์สำหรับปัญหาเดียวกันแต่ควรสังเกตการเชื่อมต่อโครูทีนที่กล่าวถึงในลิงก์นั้น
การประเมินความเกียจคร้านคือการใช้เหตุผลที่เท่าเทียมกันของคนไม่ดี (ซึ่งอาจคาดหมายได้ว่าเป็นการอนุมานคุณสมบัติของรหัสจากคุณสมบัติของประเภทและการดำเนินการที่เกี่ยวข้อง)
ตัวอย่างที่ทำงานได้ดี: sum . take 10 $ [1..10000000000]
. ซึ่งเราไม่รังเกียจที่จะลดจำนวนลงเป็นจำนวน 10 ตัวแทนที่จะเป็นการคำนวณตัวเลขโดยตรงและง่ายๆเพียงตัวเดียว หากไม่มีการประเมินที่ขี้เกียจแน่นอนสิ่งนี้จะสร้างรายการขนาดมหึมาในหน่วยความจำเพียงเพื่อใช้ 10 องค์ประกอบแรก แน่นอนว่ามันจะช้ามากและอาจทำให้เกิดข้อผิดพลาดหน่วยความจำไม่เพียงพอ
ตัวอย่างที่ไม่ดีเท่าที่เราต้องการ: sum . take 1000000 . drop 500 $ cycle [1..20]
. ซึ่งจะรวมตัวเลข 1,000,000 จริงแม้ว่าจะอยู่ในลูปแทนที่จะอยู่ในรายการก็ตาม ถึงกระนั้นก็ควรลดลงเหลือเพียงการคำนวณตัวเลขโดยตรงเพียงรายการเดียวโดยมีเงื่อนไขและสูตรไม่กี่สูตร ซึ่งจะดีกว่ามากจากนั้นสรุปตัวเลข 1 000 000 แม้ว่าจะอยู่ในวงและไม่อยู่ในรายการ (เช่นหลังจากการเพิ่มประสิทธิภาพการตัดไม้ทำลายป่า)
อีกประการหนึ่งคือทำให้สามารถเขียนโค้ดในรูปแบบtail recursion modulo cons ได้และใช้งานได้จริง
ถ้าตาม "การประเมินแบบขี้เกียจ" คุณหมายถึงว่าชอบในบูลีนแบบผสมเช่นใน
if (ConditionA && ConditionB) ...
คำตอบก็คือยิ่ง CPU ทำงานน้อยลงโปรแกรมก็จะทำงานได้เร็วขึ้น ... และหากคำสั่งการประมวลผลชิ้นส่วนจะไม่มีผลกระทบต่อผลลัพธ์ของโปรแกรมก็ไม่จำเป็น (ดังนั้นจึงเป็นการสิ้นเปลือง ของเวลา) เพื่อดำเนินการต่อไป ...
ถ้า otoh คุณหมายถึงสิ่งที่ฉันรู้จักในชื่อ "lazy initializers" ดังใน:
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));
}
}
}
เทคนิคนี้ช่วยให้รหัสไคลเอ็นต์โดยใช้คลาสเพื่อหลีกเลี่ยงความจำเป็นในการเรียกฐานข้อมูลสำหรับบันทึกข้อมูลหัวหน้างานยกเว้นเมื่อลูกค้าที่ใช้วัตถุพนักงานต้องการการเข้าถึงข้อมูลของหัวหน้างาน ... สิ่งนี้ทำให้กระบวนการสร้างอินสแตนซ์ของพนักงานเร็วขึ้น และเมื่อคุณต้องการ Supervisor การเรียกครั้งแรกไปยังคุณสมบัติ Supervisor จะทริกเกอร์การเรียกฐานข้อมูลและข้อมูลจะถูกดึงและพร้อมใช้งาน ...
ตัดตอนมาจากฟังก์ชันลำดับที่สูงกว่า
ลองหาจำนวนที่มากที่สุดที่ต่ำกว่า 100,000 หารด้วย 3829 หารด้วย 3829 เราจะกรองความเป็นไปได้ชุดหนึ่งที่เรารู้ว่าคำตอบอยู่
largestDivisible :: (Integral a) => a
largestDivisible = head (filter p [100000,99999..])
where p x = x `mod` 3829 == 0
อันดับแรกเราทำรายการตัวเลขทั้งหมดที่ต่ำกว่า 100,000 จากมากไปหาน้อย จากนั้นเรากรองตามเพรดิเคตของเราและเนื่องจากตัวเลขถูกเรียงลำดับจากมากไปหาน้อยจำนวนมากที่สุดที่ตรงตามเพรดิเคตของเราคือองค์ประกอบแรกของรายการที่กรอง เราไม่จำเป็นต้องใช้รายการ จำกัด สำหรับชุดเริ่มต้นของเราด้วยซ้ำ นั่นคือความเกียจคร้านในการดำเนินการอีกครั้ง เนื่องจากเราใช้เฉพาะส่วนหัวของรายการที่กรองแล้วจึงไม่สำคัญว่ารายการที่กรองนั้นจะ จำกัด หรือไม่มีที่สิ้นสุด การประเมินจะหยุดลงเมื่อพบวิธีแก้ปัญหาที่เพียงพอเป็นอันดับแรก