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


119

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

หมายเหตุ: ฉันไม่ได้หมายถึงการท่องจำ

คำตอบ:


96

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

นอกจากนี้ยังช่วยให้มีสิ่งดีๆเช่นรายการที่ไม่มีที่สิ้นสุด ฉันไม่มีรายการที่ไม่มีที่สิ้นสุดในภาษาเช่น C แต่ใน Haskell นั้นไม่มีปัญหา รายการที่ไม่มีที่สิ้นสุดมักใช้บ่อยในบางพื้นที่ของคณิตศาสตร์ดังนั้นจึงมีประโยชน์ที่จะมีความสามารถในการจัดการ


6
Python มีรายการที่ไม่สิ้นสุดที่ประเมินอย่างเกียจคร้านผ่านตัวทำซ้ำ
Mark Cidade

4
คุณสามารถจำลองรายการที่ไม่มีที่สิ้นสุดใน Python ได้โดยใช้ตัวสร้างและนิพจน์ตัวสร้าง (ซึ่งทำงานในลักษณะเดียวกับการทำความเข้าใจรายการ): python.org/doc/2.5.2/ref/genexpr.html
John Montgomery

24
เครื่องกำเนิดไฟฟ้าทำให้รายการขี้เกียจเป็นเรื่องง่ายใน Python แต่เทคนิคการประเมินและโครงสร้างข้อมูลอื่น ๆ ที่ขี้เกียจนั้นมีความสง่างามน้อยกว่าอย่างเห็นได้ชัด
Peter Burns

3
ฉันเกรงว่าจะไม่เห็นด้วยกับคำตอบนี้ ฉันเคยคิดว่าความขี้เกียจเป็นเรื่องของประสิทธิภาพ แต่หลังจากใช้ Haskell เป็นจำนวนมากแล้วจึงเปลี่ยนมาใช้ Scala และเปรียบเทียบประสบการณ์ฉันต้องบอกว่าความขี้เกียจมีความสำคัญบ่อยครั้ง แต่ไม่ค่อยเกิดจากประสิทธิภาพ ฉันคิดว่า Edward Kmett ให้ความสำคัญกับเหตุผลที่แท้จริง
Owen

3
ฉันไม่เห็นด้วยในทำนองเดียวกันในขณะที่ไม่มีความคิดที่ชัดเจนเกี่ยวกับรายการที่ไม่มีที่สิ้นสุดใน C เนื่องจากการประเมินอย่างเข้มงวดคุณสามารถเล่นเคล็ดลับเดียวกันในภาษาอื่น ๆ ได้อย่างง่ายดาย (และแน่นอนในการเลียนแบบภาษาขี้เกียจส่วนใหญ่) โดยใช้ thunks และส่งผ่านฟังก์ชัน ตัวชี้เพื่อทำงานกับคำนำหน้า จำกัด ของโครงสร้างอนันต์ที่สร้างขึ้นโดยนิพจน์ที่คล้ายกัน
Kristopher Micinski

71

ตัวอย่างที่เป็นประโยชน์ของการประเมินแบบขี้เกียจคือการใช้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]) ซึ่งเพียงพอแล้ว เนื่องจากขี้เกียจประเมินผลที่เหลือจึงไม่ถูกจัดเรียงช่วยประหยัดเวลาในการคำนวณได้มาก

แน่นอนว่านี่เป็นตัวอย่างที่ง่ายมาก แต่ความเกียจคร้านทำงานในลักษณะเดียวกันกับโปรแกรมที่มีขนาดใหญ่มาก

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


19
โดยทั่วไปtake k $ quicksort listจะใช้เวลาเพียง O (n + บันทึก k k) n = length listเวลาที่ ด้วยการเปรียบเทียบแบบไม่ขี้เกียจสิ่งนี้จะใช้เวลา O (n log n) เสมอ
ephemient

@ephemient คุณไม่ได้หมายถึง O (nk log k) เหรอ?
MaiaVictor

1
@Viclib ไม่ฉันหมายถึงสิ่งที่ฉันพูด
ephemient

@ephemient แล้วฉันคิดว่าฉันไม่เข้าใจเศร้า
MaiaVictor

2
@Viclib อัลกอริทึมการเลือกสำหรับการค้นหาองค์ประกอบ k ด้านบนจาก n คือ O (n + k log k) เมื่อคุณใช้ Quicksort ในภาษาขี้เกียจและประเมินได้ไกลพอที่จะกำหนดองค์ประกอบ k แรกเท่านั้น (หยุดการประเมินผลหลังจากนั้น) จะทำการเปรียบเทียบแบบเดียวกันกับที่อัลกอริทึมการเลือกแบบไม่ขี้เกียจ
ephemient

70

ฉันพบว่าการประเมินแบบขี้เกียจมีประโยชน์สำหรับหลาย ๆ อย่าง

ประการแรกภาษาขี้เกียจที่มีอยู่ทั้งหมดเป็นภาษาที่บริสุทธิ์เนื่องจากเป็นเรื่องยากมากที่จะให้เหตุผลเกี่ยวกับผลข้างเคียงในภาษาขี้เกียจ

ภาษาบริสุทธิ์ช่วยให้คุณสามารถหาเหตุผลเกี่ยวกับคำจำกัดความของฟังก์ชันโดยใช้การให้เหตุผลเชิงเท่าเทียมกัน

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 หรือสองข้อเนื่องจากไม่มีความเกียจคร้านและขั้นตอนการทำงานโดยการเปรียบเทียบรับสัมภาระที่เข้มงวดจำนวนมาก)


5
ดีมาก; นี่คือคำตอบที่แท้จริง ฉันเคยคิดว่ามันเป็นเรื่องของประสิทธิภาพ (ชะลอการคำนวณในภายหลัง) จนกระทั่งฉันใช้ Haskell เป็นจำนวนมากและเห็นว่านั่นไม่ใช่เหตุผลเลย
Owen

11
นอกจากนี้ในทางเทคนิคแล้วภาษาขี้เกียจจะต้องบริสุทธิ์ (R เป็นตัวอย่าง) แต่ก็เป็นความจริงที่ภาษาขี้เกียจที่ไม่บริสุทธิ์สามารถทำสิ่งที่แปลกประหลาดได้ (R เป็นตัวอย่าง)
Owen

4
แน่นอนว่ามี ในภาษาที่เข้มงวดการเรียกซ้ำletเป็นสัตว์ร้ายที่อันตรายในรูปแบบ R6RS จะทำให้สุ่ม#fปรากฏในคำของคุณได้ทุกที่ที่ผูกปมอย่างเคร่งครัดนำไปสู่วงจร! ไม่มีจุดมุ่งหมายในการเล่นสำนวน แต่letการผูกแบบเรียกซ้ำอย่างเคร่งครัดนั้นสมเหตุสมผลในภาษาที่เกียจคร้าน ความเข้มงวดยังทำให้ความจริงที่ว่าwhereไม่มีวิธีใดที่จะสั่งให้เกิดผลกระทบที่สัมพันธ์กันได้เลยยกเว้นโดย SCC เป็นการสร้างระดับคำสั่งผลของมันอาจเกิดขึ้นในลำดับใดก็ได้อย่างเคร่งครัดและแม้ว่าคุณจะมีภาษาที่บริสุทธิ์ก็ตาม#fปัญหา. ไขwhereปริศนารหัสของคุณด้วยความกังวลที่ไม่เกี่ยวข้องกับท้องถิ่น
Edward KMETT

2
คุณช่วยอธิบายได้ว่าความขี้เกียจช่วยหลีกเลี่ยงข้อ จำกัด ด้านคุณค่าได้อย่างไร? ฉันยังไม่เข้าใจเรื่องนี้
Tom Ellis

3
@PaulBone คุณกำลังพูดถึงอะไร? ความเกียจคร้านมีส่วนเกี่ยวข้องกับโครงสร้างการควบคุมมากมาย หากคุณกำหนดโครงสร้างการควบคุมของคุณเองในภาษาที่เข้มงวดมันจะต้องใช้แลมบ์ดาจำนวนมากหรือคล้ายกันไม่เช่นนั้นมันจะดูด เพราะifFunc(True, x, y)เป็นไปได้ในการประเมินทั้งสองxและแทนเพียงy x
อัฒภาค

28

มีความแตกต่างระหว่างการประเมินคำสั่งซื้อปกติกับการประเมินแบบขี้เกียจ (เช่นเดียวกับใน 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)


25

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


การเปรียบเทียบกับ GC ช่วยฉันได้เล็กน้อย แต่คุณช่วยยกตัวอย่าง "ลบรหัสสำเร็จรูป" ได้ไหม
อับดุล

1
@Abdul เป็นตัวอย่างที่ผู้ใช้ ORM คุ้นเคย: การเชื่อมโยงที่ขี้เกียจกำลังโหลด มันโหลดการเชื่อมโยงจาก DB "ทันเวลา" และในขณะเดียวกันก็ปล่อยนักพัฒนาออกจากความจำเป็นที่จะต้องระบุอย่างชัดเจนว่าจะโหลดเมื่อใดและจะแคชอย่างไร (นี่คือต้นแบบที่ฉันหมายถึง) นี่เป็นอีกตัวอย่างหนึ่ง: projectlombok.org/features/GetterLazy.html
Alexey

25

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

Richard Bird, John Hughes และส่วนขยายที่น้อยกว่านั้น Ralf Hinze สามารถทำสิ่งที่น่าอัศจรรย์ด้วยการประเมินที่ขี้เกียจ การอ่านงานของพวกเขาจะช่วยให้คุณรู้สึกซาบซึ้ง ดีจุดเริ่มต้นที่มีซูโดกุของนกที่งดงามแก้และกระดาษฮิวจ์สในเรื่องการเขียนโปรแกรมทำไมฟังก์ชั่น


ไม่เพียงบังคับให้พวกเขารักษาภาษาให้บริสุทธิ์ แต่ยังอนุญาตให้พวกเขาทำเช่นนั้นเมื่อ (ก่อนหน้านี้มีการแนะนำIOmonad) ลายเซ็นmainจะเป็นString -> Stringและคุณสามารถเขียนโปรแกรมโต้ตอบได้อย่างถูกต้อง
leftaround ประมาณ

@leftaroundabout: อะไรคือการหยุดภาษาที่เข้มงวดจากการบังคับให้เอฟเฟกต์ทั้งหมดเป็นIOmonad?
Tom Ellis

13

พิจารณาโปรแกรม tic-tac-toe มีสี่ฟังก์ชั่น:

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

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

ตอนนี้ให้ลองใช้หมากรุกแทน tic-tac-toe ในภาษา "กระตือรือร้น" (เช่นทั่วไป) สิ่งนี้จะใช้ไม่ได้เพราะแผนผังการย้ายจะไม่พอดีกับหน่วยความจำ ดังนั้นตอนนี้การประเมินบอร์ดและฟังก์ชันการสร้างการเคลื่อนย้ายจึงจำเป็นต้องผสมกับทรีการย้ายและตรรกะขั้นต่ำเนื่องจากต้องใช้ตรรกะขั้นต่ำเพื่อตัดสินใจว่าจะสร้างการเคลื่อนไหวใด โครงสร้างโมดูลาร์ที่สะอาดสวยงามของเราหายไป

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


1
[ในภาษาที่ "กระตือรือร้น" (เช่นทั่วไป) สิ่งนี้จะใช้ไม่ได้เพราะต้นไม้ย้ายจะไม่พอดีกับหน่วยความจำ] - สำหรับ Tic-Tac-Toe มันจะแน่นอน มีตำแหน่งมากที่สุด 3 ** 9 = 19683 ตำแหน่งในการจัดเก็บ ถ้าเราจัดเก็บแต่ละอันในขนาด 50 ไบต์ที่ฟุ่มเฟือยนั่นคือเกือบหนึ่งเมกะไบต์ ไม่มีอะไรเลย ...
Jonas Kölker

6
ใช่นั่นคือประเด็นของฉัน ภาษาที่กระตือรือร้นสามารถมีโครงสร้างที่ชัดเจนสำหรับเกมที่ไม่สำคัญ แต่ต้องประนีประนอมโครงสร้างนั้นเพื่อสิ่งที่เป็นจริง ภาษาขี้เกียจไม่มีปัญหานั้น
Paul Johnson

3
อย่างไรก็ตามเพื่อความเป็นธรรมการประเมินแบบขี้เกียจอาจทำให้เกิดปัญหาด้านความจำของตัวเอง ไม่ใช่เรื่องแปลกที่ผู้คนจะถามว่าทำไม haskell ถึงระเบิดความทรงจำสำหรับบางสิ่งที่ในการประเมินอย่างกระตือรือร้นจะมีการใช้หน่วยความจำ O (1)
RHSeeger

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

12

นี่คืออีกสองประเด็นที่ฉันไม่เชื่อว่าจะถูกนำมาอภิปราย

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

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


10

เมื่อคุณเปิดคอมพิวเตอร์และ Windows ละเว้นจากการเปิดทุกไดเร็กทอรีเดียวบนฮาร์ดไดรฟ์ของคุณใน Windows Explorer และละเว้นจากการเปิดทุกโปรแกรมที่ติดตั้งบนคอมพิวเตอร์ของคุณจนกว่าคุณจะระบุว่าจำเป็นต้องมีไดเร็กทอรีบางรายการหรือจำเป็นต้องมีโปรแกรมบางอย่าง คือการประเมินแบบ "ขี้เกียจ"

การประเมินแบบ "ขี้เกียจ" กำลังดำเนินการเมื่อและตามที่จำเป็น จะมีประโยชน์เมื่อเป็นคุณลักษณะของภาษาโปรแกรมหรือไลบรารีเพราะโดยทั่วไปแล้วการประเมินแบบขี้เกียจด้วยตัวคุณเองจะทำได้ยากกว่าการคำนวณล่วงหน้าทุกอย่าง


1
บางคนอาจบอกว่านั่นคือ "การประหารแบบขี้เกียจ" จริงๆ ความแตกต่างนั้นไร้สาระจริงๆยกเว้นในภาษาที่บริสุทธิ์พอสมควรเช่น Haskell; แต่ความแตกต่างก็คือไม่ใช่แค่การคำนวณที่ล่าช้าเท่านั้น แต่ยังรวมถึงผลข้างเคียงที่เกี่ยวข้องด้วย (เช่นการเปิดและอ่านไฟล์)
Owen

8

พิจารณาสิ่งนี้:

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

วิธี doSomething () จะถูกดำเนินการก็ต่อเมื่อ conditionOne เป็นจริงและ conditionTwo เป็นจริง ในกรณีที่ conditionOne เป็นเท็จทำไมคุณต้องคำนวณผลลัพธ์ของ conditionTwo การประเมิน conditionTwo จะเสียเวลาในกรณีนี้โดยเฉพาะอย่างยิ่งถ้าเงื่อนไขของคุณเป็นผลมาจากกระบวนการวิธีการบางอย่าง

นั่นคือตัวอย่างหนึ่งของความสนใจในการประเมินที่ขี้เกียจ ...


ฉันคิดว่านั่นคือการลัดวงจรไม่ใช่การประเมินแบบขี้เกียจ
Thomas Owens

2
เป็นการประเมินที่ขี้เกียจเนื่องจาก conditionTwo จะคำนวณเฉพาะในกรณีที่จำเป็นจริงๆ (กล่าวคือถ้า conditionOne เป็นจริง)
Romain Linsolas

7
ฉันคิดว่าการลัดวงจรเป็นกรณีที่ลดลงของการประเมินแบบขี้เกียจ แต่ก็ไม่ใช่วิธีทั่วไปที่จะคิดเกี่ยวกับเรื่องนี้
rmeador

19
ในความเป็นจริงแล้วการลัดวงจรเป็นกรณีพิเศษของการประเมินแบบขี้เกียจ การประเมินแบบเกียจคร้านครอบคลุมมากกว่าการลัดวงจรอย่างเห็นได้ชัด หรือการลัดวงจรมีอะไรบ้างในการประเมินที่ขี้เกียจ?
yfeldblum

2
@ จูเลียต: คุณมีคำจำกัดความที่ชัดเจนว่า 'ขี้เกียจ' ตัวอย่างของฟังก์ชันที่รับพารามิเตอร์สองตัวไม่เหมือนกับคำสั่งลัดวงจร if คำสั่งลัดวงจรหากหลีกเลี่ยงการคำนวณที่ไม่จำเป็น ฉันคิดว่าการเปรียบเทียบที่ดีกว่ากับตัวอย่างของคุณน่าจะเป็นตัวดำเนินการของ Visual Basic "andalso" ซึ่งบังคับให้ประเมินเงื่อนไขทั้งสอง

8
  1. สามารถเพิ่มประสิทธิภาพ นี่คือสิ่งที่ดูชัดเจน แต่จริงๆแล้วมันไม่สำคัญที่สุด (โปรดทราบว่าความเกียจคร้านสามารถฆ่าประสิทธิภาพได้เช่นกัน - ข้อเท็จจริงนี้ไม่ชัดเจนในทันทีอย่างไรก็ตามการจัดเก็บผลลัพธ์ชั่วคราวจำนวนมากแทนที่จะคำนวณทันทีคุณจะสามารถใช้ RAM ได้มากพอสมควร)

  2. ช่วยให้คุณกำหนดโครงสร้างการควบคุมโฟลว์ในโค้ดระดับผู้ใช้ปกติแทนที่จะเป็นฮาร์ดโค้ดในภาษา (เช่น Java มีforลูป Haskell มีforฟังก์ชัน Java มีการจัดการข้อยกเว้น Haskell มี monad ข้อยกเว้นหลายประเภท C # มีgoto; Haskell มี monad ต่อเนื่อง ... )

  3. ช่วยให้คุณแยกอัลกอริทึมในการสร้างข้อมูลจากอัลกอริทึมเพื่อตัดสินใจว่าจะสร้างข้อมูลเท่าใด คุณสามารถเขียนฟังก์ชันหนึ่งที่สร้างรายการผลลัพธ์ที่ไม่มีที่สิ้นสุดตามแนวคิดและอีกฟังก์ชันหนึ่งที่ประมวลผลรายการนี้ได้มากเท่าที่จะตัดสินใจได้ ยิ่งไปกว่านั้นคุณสามารถมีฟังก์ชันเครื่องกำเนิดไฟฟ้าห้าฟังก์ชันและฟังก์ชันสำหรับผู้บริโภคห้าฟังก์ชันและคุณสามารถสร้างชุดค่าผสมใด ๆ ได้อย่างมีประสิทธิภาพแทนที่จะเขียนโค้ดด้วยตนเอง 5 x 5 = 25 ฟังก์ชันที่รวมการกระทำทั้งสองอย่างพร้อมกัน (!) เราทุกคนรู้ดีว่าการแยกส่วนเป็นสิ่งที่ดี

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


6

ประโยชน์อย่างมากอย่างหนึ่งของความเกียจคร้านคือความสามารถในการเขียนโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปโดยมีขอบเขตการตัดจำหน่ายที่สมเหตุสมผล ตัวอย่างง่ายๆคือสแต็กที่ไม่เปลี่ยนรูป (ใช้ 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 อย่างชัดเจน


5

ตัวอย่างข้อมูลนี้แสดงความแตกต่างระหว่างการประเมินแบบขี้เกียจและไม่ขี้เกียจ แน่นอนว่าฟังก์ชัน 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)

5

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

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

ตัวอย่างนี้ใช้ในฟังก์ชันการเปิดใช้งานและในอัลกอริทึมการเรียนรู้ backpropagation (ฉันสามารถโพสต์ลิงก์ได้เพียงสองลิงก์ดังนั้นคุณจะต้องค้นหาlearnPatฟังก์ชันในAI.Instinct.Train.Deltaโมดูลด้วยตัวเอง) ตามเนื้อผ้าทั้งสองต้องการอัลกอริทึมซ้ำที่ซับซ้อนกว่านี้มาก


4

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

ใน 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)

3

ฉันไม่รู้ว่าตอนนี้คุณคิดยังไง แต่ฉันคิดว่าการคิดว่าการประเมินแบบขี้เกียจเป็นปัญหาของห้องสมุดนั้นมีประโยชน์มากกว่าฟีเจอร์ด้านภาษา

ฉันหมายความว่าในภาษาที่เข้มงวดฉันสามารถใช้การประเมินผลแบบขี้เกียจได้โดยการสร้างโครงสร้างข้อมูลบางอย่างและในภาษาขี้เกียจ (อย่างน้อยก็ Haskell) ฉันสามารถขอความเข้มงวดได้เมื่อฉันต้องการ ดังนั้นตัวเลือกภาษาไม่ได้ทำให้โปรแกรมของคุณขี้เกียจหรือไม่ขี้เกียจ แต่มีผลกับสิ่งที่คุณได้รับโดยค่าเริ่มต้น

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


1
การใช้การประเมินแบบขี้เกียจในภาษาที่เข้มงวดมักเป็น Turing Tarpit
itsbruce

2

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

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;
}

เมื่อคุณเริ่มใช้งานคุณจะเห็นโอกาสในการใช้งานบ่อยขึ้นเรื่อย ๆ


2

หากไม่มีการประเมินที่ขี้เกียจคุณจะไม่ได้รับอนุญาตให้เขียนสิ่งนี้:

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

ดี imo นี่เป็นความคิดที่ไม่ดีที่จะทำเช่นนั้น แม้ว่ารหัสนี้อาจจะถูกต้อง (ขึ้นอยู่กับสิ่งที่คุณพยายามจะบรรลุ) แต่ก็ยากที่จะอ่านซึ่งเป็นสิ่งที่ไม่ดีเสมอ
Brann

12
ฉันไม่คิดอย่างนั้น โครงสร้างมาตรฐานใน C และเครือญาติ
Paul Johnson

นี่คือตัวอย่างของการประเมินการลัดวงจรไม่ใช่การประเมินแบบขี้เกียจ หรือนั่นคือสิ่งเดียวกันอย่างมีประสิทธิภาพ?
RufusVS

2

เหนือสิ่งอื่นใดภาษาขี้เกียจอนุญาตให้มีโครงสร้างข้อมูลที่ไม่มีที่สิ้นสุดหลายมิติ

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

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


2

การประเมินความเกียจคร้านคือการใช้เหตุผลที่เท่าเทียมกันของคนไม่ดี (ซึ่งอาจคาดหมายได้ว่าเป็นการอนุมานคุณสมบัติของรหัสจากคุณสมบัติของประเภทและการดำเนินการที่เกี่ยวข้อง)

ตัวอย่างที่ทำงานได้ดี: 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 ได้และใช้งานได้จริง

cf เลย คำตอบที่เกี่ยวข้อง


1

ถ้าตาม "การประเมินแบบขี้เกียจ" คุณหมายถึงว่าชอบในบูลีนแบบผสมเช่นใน

   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 จะทริกเกอร์การเรียกฐานข้อมูลและข้อมูลจะถูกดึงและพร้อมใช้งาน ...


0

ตัดตอนมาจากฟังก์ชันลำดับที่สูงกว่า

ลองหาจำนวนที่มากที่สุดที่ต่ำกว่า 100,000 หารด้วย 3829 หารด้วย 3829 เราจะกรองความเป็นไปได้ชุดหนึ่งที่เรารู้ว่าคำตอบอยู่

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

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

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