ข้อเสียใดที่อนุญาตให้กำจัดหางแบบโมดูโลแบบเรียกซ้ำแบบหางได้?


14

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

foo(...):
    # ...
    return foo(...)

ฉันยังเข้าใจว่าเป็นกรณีพิเศษฟังก์ชั่นยังสามารถเขียนใหม่ถ้าโทร recursive consถูกห่อในการเรียกไปยัง

foo(...):
    # ...
    return (..., foo(...))

คุณสมบัติอะไรที่consอนุญาตให้ทำเช่นนี้? ฟังก์ชั่นอื่นใดนอกจากconsสามารถตัดหางเรียกซ้ำโดยไม่ทำลายความสามารถของเราในการเขียนซ้ำซ้ำ?

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

pow(x, n):
    if n == 0: return 1
    else if n == 1: return x
    else: return x * pow(x, n-1)

1
ในลิงก์ผู้รวบรวมผู้รวบรวม Godbolt ของคุณฟังก์ชั่นของคุณมีif(n==0) return 0;(ไม่คืนค่า 1 เช่นเดียวกับในคำถามของคุณ) x^0 = 1นั่นเป็นข้อผิดพลาด ไม่ใช่ว่ามันสำคัญสำหรับส่วนที่เหลือของคำถาม แต่; asm ซ้ำจะตรวจสอบกรณีพิเศษก่อน แต่ที่แปลกการใช้งานซ้ำแนะนำจำนวน1 * xที่ไม่ได้อยู่ในแหล่งที่มาแม้ว่าเราจะทำfloatรุ่น gcc.godbolt.org/z/eqwine (และ gcc ทำได้สำเร็จเท่านั้น-ffast-math)
Peter Cordes

@ PeterCordes จับได้ดี return 0ได้รับการแก้ไข การคูณด้วย 1 นั้นน่าสนใจ ฉันไม่แน่ใจว่าจะทำอย่างไร
Maxpm

ฉันคิดว่ามันเป็นผลข้างเคียงของวิธีที่ GCC เปลี่ยนเมื่อเปลี่ยนเป็นลูป เห็นได้ชัดว่า gcc มีการเพิ่มประสิทธิภาพที่ไม่ได้รับที่นี่เช่นหายไปfloatโดยไม่ใช้-ffast-mathแม้ว่ามันจะมีค่าเท่ากันทุกครั้ง (ยกเว้น 1.0f` ซึ่งอาจเป็นจุดที่ติดอยู่?)
Peter Cordes

คำตอบ:


12

แม้ว่า GCC จะใช้กฎ Ad-hoc แต่คุณสามารถดูกฎเหล่านี้ได้ด้วยวิธีต่อไปนี้ ฉันจะใช้powเพื่ออธิบายเนื่องจากคุณfooมีการกำหนดไว้อย่างคลุมเครือ นอกจากนี้fooที่ดีที่สุดอาจจะเข้าใจว่าเป็นตัวอย่างของการเพิ่มประสิทธิภาพที่ผ่านมาโทรที่มีความเคารพต่อตัวแปรเดียวที่ได้รับมอบหมายเป็นภาษาออนซ์มีและเป็นที่กล่าวถึงในแนวคิดเทคนิคและรูปแบบของการเขียนโปรแกรมคอมพิวเตอร์ ประโยชน์ของการใช้ตัวแปรการกำหนดค่าเดียวคือมันช่วยให้เหลืออยู่ภายในกระบวนทัศน์การเขียนโปรแกรมที่ประกาศ โดยพื้นฐานแล้วคุณสามารถให้แต่ละฟิลด์ของการfooส่งคืนstruct แสดงโดยตัวแปรการกำหนดค่าเดียวที่ถูกส่งผ่านไปแล้วเพื่อfooเป็นอาร์กิวเมนต์เพิ่มเติม fooจากนั้นจะกลายเป็นหางซ้ำvoidฟังก์ชั่นกลับมา ไม่จำเป็นต้องมีความฉลาดเฉพาะสำหรับเรื่องนี้

กลับไปpowก่อนเปลี่ยนเป็นรูปแบบผ่านต่อเนื่อง powกลายเป็น:

pow(x, n):
    return pow2(x, n, x => x)

pow2(x, n, k):
    if n == 0: return k(1)
    else if n == 1: return k(x)
    else: return pow2(x, n-1, y => k(x*y))

การโทรทั้งหมดเป็นการโทรแบบหางทันที อย่างไรก็ตามสแต็กควบคุมได้ถูกย้ายไปยังสภาพแวดล้อมที่ถูกจับในการปิดที่แสดงถึงความต่อเนื่อง

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

pow(x, n):
    return pow2(x, n, Nil)

pow2(x, n, k):
    if n == 0: return applyPow(k, 1)
    else if n == 1: return applyPow(k, x)
    else: return pow2(x, n-1, Cons(x, k))

applyPow(k, acc):
    match k with:
        case Nil: return acc
        case Cons(x, k):
            return applyPow(k, x*acc)

สิ่งที่applyPow(k, acc)ไม่สามารถใช้รายการเช่นหนังสือฟรีเช่นและทำให้มันกลายเป็นk=Cons(x, Cons(x, Cons(x, Nil))) x*(x*(x*acc))แต่เนื่องจาก*เป็นเชื่อมโยงและโดยทั่วไปรูปแบบหนังสือกับหน่วย1เราสามารถ reassociate นี้ลง((x*x)*x)*accและเพื่อความง่ายตะปูในการเริ่มต้นการผลิต1 สิ่งสำคัญคือการที่เราสามารถจริงบางส่วนคำนวณผลแม้กระทั่งก่อนที่เรามี(((1*x)*x)*x)*acc accนั่นหมายความว่าแทนที่จะผ่านkรายการซึ่งเป็น "ไวยากรณ์" ที่ไม่สมบูรณ์ที่เราจะ "ตีความ" ในตอนท้ายเราสามารถ "ตีความ" มันได้เมื่อเราไป ผลที่สุดคือเราสามารถแทนที่Nilด้วยหน่วยของ monoid 1ในกรณีนี้และConsด้วยการทำงานของ monoid *และตอนนี้kแสดงถึง "ผลิตภัณฑ์ที่กำลังทำงาน"applyPow(k, acc)จากนั้นกลายเป็นk*accสิ่งที่เราสามารถอินไลน์กลับเข้ามาpow2และทำให้การผลิตง่ายขึ้น:

pow(x, n):
    return pow2(x, n, 1)

pow2(x, n, k):
    if n == 0: return k
    else if n == 1: return k*x
    else: return pow2(x, n-1, k*x)

หาง recursive powรุ่นสไตล์สะสมผ่านของเดิม

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

หากคุณต้องการไร้สาระคุณสามารถตรวจสอบ Paper Recycling Continuationsซึ่งใช้ CPS และการเป็นตัวแทนของ continuations เป็นข้อมูล แต่ทำสิ่งที่คล้ายกับ แต่แตกต่างจาก tail-recursion-modulo-cons สิ่งนี้อธิบายถึงวิธีที่คุณอาจสร้างอัลกอริทึมการย้อนกลับของตัวชี้โดยการแปลง

รูปแบบนี้ของ CPS เปลี่ยนและ defunctionalizing เป็นเครื่องมือที่มีประสิทธิภาพมากในการทำความเข้าใจและนำมาใช้เพื่อผลดีในชุดของเอกสารที่ผมรายการที่นี่


เทคนิคที่ GCC ใช้แทนรูปแบบการส่งต่อที่คุณแสดงที่นี่คือฉันเชื่อว่าแบบฟอร์มการมอบหมายแบบคงที่เดียว
Davislor

@Davislor ในขณะที่เกี่ยวข้องกับ CPS, SSA จะไม่ส่งผลกระทบต่อการควบคุมการไหลของโพรซีเดอร์หรือจะไม่ทำการเรียงสแต็กอีกครั้ง (หรือแนะนำโครงสร้างข้อมูลที่จะต้องมีการจัดสรรแบบไดนามิก) ที่เกี่ยวข้องกับ SSA, CPS "ทำมากเกินไป" ซึ่งเป็นสาเหตุที่ฟอร์มปกติสำหรับผู้ดูแลระบบ (ANF) นั้นเหมาะสมกับ SSA มากขึ้น ดังนั้น GCC ใช้ SSA แต่ SSA ไม่นำไปสู่การควบคุมสแต็กที่สามารถดูได้เป็นโครงสร้างข้อมูลที่จัดการได้
Derek Elkins ออกจาก SE

ขวา. ฉันตอบกลับว่า“ ฉันไม่ได้บอกว่า GCC ทำทุกอย่างด้วยเหตุผลในเวลารวบรวม ฉันไม่รู้ว่า GCC ใช้ตรรกะอะไร” คำตอบของฉันในทำนองเดียวกันก็แสดงให้เห็นว่าการเปลี่ยนแปลงเป็นเหตุผลทางทฤษฎีไม่ได้บอกว่ามันเป็นวิธีการใช้งานที่คอมไพเลอร์ที่กำหนดใช้ (ถึงแม้ว่าจะเป็นคุณรู้ว่าคอมไพเลอร์หลายคนทำเปลี่ยนโปรแกรมลงในระหว่างการเพิ่มประสิทธิภาพของ CPS.)
Davislor

8

ฉันจะไปรอบ ๆ พุ่มไม้สักพัก แต่ก็มีประเด็น

semigroups

คำตอบคือเชื่อมโยงทรัพย์สินของการดำเนินงานที่ลดลงไบนารี

มันค่อนข้างเป็นนามธรรม แต่การคูณเป็นตัวอย่างที่ดี ถ้าx , YและZคือบางส่วนจำนวนธรรมชาติ (หรือจำนวนเต็มหรือตัวเลขที่มีเหตุผลหรือตัวเลขจริงหรือตัวเลขที่ซับซ้อนหรือN × Nการฝึกอบรมหรือใด ๆ ของทั้งกลุ่มสิ่งที่มากขึ้น) แล้วx × Yเป็นชนิดเดียวกัน จำนวนเป็นทั้งxและy ที่ เราเริ่มต้นด้วยตัวเลขสองตัวดังนั้นมันจึงเป็นการดำเนินการแบบไบนารีและได้หนึ่งดังนั้นเราจึงลดจำนวนของจำนวนที่เรามีหนึ่งทำให้การดำเนินการลดลง และ ( x × y ) × zจะเท่ากับx × ( y ×เสมอ)z ) ซึ่งเป็นคุณสมบัติการเชื่อมโยง

(หากคุณรู้ทั้งหมดนี้แล้วคุณสามารถข้ามไปยังหัวข้อถัดไป)

อีกสองสามสิ่งที่คุณมักจะเห็นในวิทยาการคอมพิวเตอร์ที่ทำงานในลักษณะเดียวกัน:

  • การเพิ่มตัวเลขชนิดใด ๆ เหล่านั้นแทนการคูณ
  • การต่อสตริง ( "a"+"b"+"c"คือ"abc"คุณเริ่มต้นด้วย"ab"+"c"หรือ"a"+"bc")
  • แยกสองรายการเข้าด้วยกัน [a]++[b]++[c]เหมือนกัน[a,b,c]ทั้งจากหลังไปข้างหน้าหรือข้างหน้าไปข้างหลัง
  • consบนหัวและหางหากคุณคิดว่าหัวเป็นรายการเดี่ยว นั่นเป็นเพียงการต่อสองรายการ
  • รับสหภาพหรือจุดตัดของเซต
  • บูลีนและบูลีนหรือ
  • บิต&, |และ^
  • องค์ประกอบของฟังก์ชั่น: ( fg ) ∘ h x = f ∘ ( gh ) x = f ( g ( h ( x )))
  • สูงสุดและต่ำสุด
  • นอกจากนี้โมดูโลพี

บางสิ่งที่ไม่:

  • การลบเพราะ 1- (1-2) ≠ (1-1) -2
  • xy = tan ( x + y ) เนื่องจาก tan (π / 4 + π / 4) ไม่ได้ถูกกำหนด
  • การคูณเหนือจำนวนลบเพราะ -1 × -1 ไม่ใช่จำนวนลบ
  • การหารจำนวนเต็มซึ่งมีปัญหาทั้งหมดสามข้อ!
  • เหตุผลไม่ใช่เพราะมันมีเพียงหนึ่งตัวถูกดำเนินการไม่ใช่สอง
  • int print2(int x, int y) { return printf( "%d %d\n", x, y ); }เป็นprint2( print2(x,y), z );และprint2( x, print2(y,z) );มีเอาต์พุตที่แตกต่างกัน

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

ลองทำที่บ้าน

เท่าที่ฉันรู้เทคนิคนี้ได้รับการอธิบายเป็นครั้งแรกในปี 1974 ในเอกสารของ Daniel Friedman และ David Wise “ การเรียกซ้ำการพับแบบเทิดทูนให้กลายเป็นการวนซ้ำ”แม้ว่าพวกเขาจะสันนิษฐานว่ามีคุณสมบัติมากกว่าที่พวกเขาต้องการ

Haskell เป็นภาษาที่ยอดเยี่ยมในการอธิบายสิ่งนี้เพราะมันมีประเภทของงานSemigroupพิมพ์ในห้องสมุดมาตรฐาน มันเรียกร้องการดำเนินงานของทั่วไปผู้ประกอบการSemigroup <>เนื่องจากรายการและสตริงเป็นอินสแตนซ์ของอินสแตนซ์ของSemigroupพวกเขาจึงกำหนด<>เป็นตัวดำเนินการเรียงต่อกัน++เป็นต้น และมีการนำเข้าที่เหมาะสม[a] <> [b]เป็นนามแฝงสำหรับซึ่งเป็น[a] ++ [b][a,b]

แต่แล้วตัวเลขล่ะ เราเพียงแค่เห็นว่าประเภทที่เป็นตัวเลข semigroups ภายใต้การอย่างใดอย่างหนึ่งนอกจากหรือคูณ! เพื่อที่หนึ่งที่ได้รับจะ<>เป็นDouble? อย่างใดอย่างหนึ่ง! Haskell กำหนดประเภทProduct Double, where (<>) = (*)(นั่นคือความหมายที่เกิดขึ้นจริงใน Haskell) และยัง,Sum Doublewhere (<>) = (+)

หนึ่งรอยย่นคือคุณใช้ความจริงที่ว่า 1 คือเอกลักษณ์ของการคูณ กึ่งกลุ่มที่มีตัวตนที่เรียกว่าหนังสือและถูกกำหนดไว้ในแพคเกจ Haskell Data.Monoidซึ่งเรียกเอกลักษณ์องค์ประกอบทั่วไปของ memptytypeclass Sum, Productและรายชื่อแต่ละคนมีเอกลักษณ์องค์ประกอบ (0, 1 และ[]ตามลำดับ) เพื่อให้พวกเขามีกรณีเช่นเดียวกับMonoid Semigroup(เพื่อไม่ให้สับสนกับmonadดังนั้นเพียงลืมฉันยังนำขึ้นเหล่านั้น)

ข้อมูลเพียงพอที่จะแปลอัลกอริทึมของคุณเป็นฟังก์ชัน Haskell โดยใช้ monoids:

module StylizedRec (pow) where

import Data.Monoid as DM

pow :: Monoid a => a -> Word -> a
{- Applies the monoidal operation of the type of x, whatever that is, by
 - itself n times.  This is already in Haskell as Data.Monoid.mtimes, but
 - let’s write it out as an example.
 -}
pow _ 0 = mempty -- Special case: Return the nullary product.
pow x 1 = x      -- The base case.
pow x n = x <> (pow x (n-1)) -- The recursive case.

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

มาโหลดโปรแกรมนี้ใน GHCI แล้วดูว่ามันทำงานอย่างไร:

*StylizedRec> getProduct $ pow 2 4
16
*StylizedRec> getProduct $ pow 7 2
49

โปรดจำไว้ว่าวิธีการที่เราประกาศpowหาทั่วไปMonoidซึ่งชนิดที่เราเรียกว่าa? เราให้ข้อมูล GHCI พอจะอนุมานได้ว่าประเภทaที่นี่เป็นที่Product Integerซึ่งเป็นinstanceของMonoidที่มี<>การดำเนินงานที่เป็นจำนวนเต็มคูณ ดังนั้นpow 2 4ขยายซ้ำไป2<>2<>2<>2ซึ่งเป็นหรือ2*2*2*2 16จนถึงตอนนี้ดีมาก

แต่ฟังก์ชั่นของเราใช้การดำเนินการ monoid ทั่วไปเท่านั้น ก่อนหน้านี้ผมบอกว่ามีตัวอย่างของผู้อื่นMonoidเรียกว่าSumที่มีการดำเนินการคือ<> +เราลองได้ไหม

*StylizedRec> getSum $ pow 2 4
8
*StylizedRec> getSum $ pow 7 2
14

การขยายตัวเดียวกันในขณะนี้จะช่วยให้เราแทน2+2+2+2 2*2*2*2การคูณคือการบวกตามการยกกำลังคือการคูณ!

แต่ฉันให้อีกตัวอย่างหนึ่งของ Hoidell monoid: รายการที่มีการดำเนินการเรียงต่อกัน

*StylizedRec> pow [2] 4
[2,2,2,2]
*StylizedRec> pow [7] 2
[7,7]

เขียน[2]บอกคอมไพเลอร์ที่ว่านี้เป็นรายการ<>ที่อยู่ในรายการเป็น++เพื่อให้เป็น[2]++[2]++[2]++[2][2,2,2,2]

ในที่สุดอัลกอริทึม (สองในความเป็นจริง)

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

หากอัลกอริทึมของคุณถึงกรณีพื้นฐานและยุติรายการจะไม่ว่างเปล่า เนื่องจากเคสเทอร์มินัลส่งคืนบางสิ่งซึ่งจะเป็นองค์ประกอบสุดท้ายของรายการดังนั้นมันจะมีองค์ประกอบอย่างน้อยหนึ่งรายการ

คุณจะใช้การดำเนินการลดเลขฐานสองกับทุกองค์ประกอบของรายการตามลำดับได้อย่างไร ถูกต้องพับ ดังนั้นคุณจึงสามารถใช้แทน[x]สำหรับการxรับรายการขององค์ประกอบที่จะลดลง<>และจากนั้นพับขวาหรือซ้ายพับรายการ:

*StylizedRec> getProduct $ foldr1 (<>) $ pow [Product 2] 4
16
*StylizedRec> import Data.List
*StylizedRec Data.List> getProduct $ foldl1' (<>) $ pow [Product 2] 4
16

รุ่นที่มีfoldr1อยู่จริงในห้องสมุดมาตรฐานเช่นsconcatสำหรับSemigroupและสำหรับmconcat Monoidมันเป็นการพับที่ด้านขวาของรายการ นั่นคือมันขยายไป[Product 2,Product 2,Product 2,Product 2]2<>(2<>(2<>(2)))

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

รุ่นที่มีfoldl1'รอยพับด้านซ้ายที่ประเมินโดยเคร่งครัด กล่าวคือเป็นฟังก์ชั่นแบบเรียกซ้ำด้วยการสะสมอย่างเข้มงวด สิ่งนี้จะประเมินให้(((2)<>2)<>2)<>2คำนวณทันทีและไม่จำเป็นเมื่อจำเป็น (อย่างน้อยมีความล่าช้าไม่มีภายในพับตัวเอง:. รายการที่ถูกพับถูกสร้างขึ้นที่นี่โดยฟังก์ชั่นอื่น ๆ ที่อาจจะมีการประเมินผลขี้เกียจ) ดังนั้นคำนวณพับ(4<>2)<>2แล้วทันทีคำนวณแล้ว8<>2 16นี่คือเหตุผลที่เราต้องการให้การดำเนินการเชื่อมโยงกัน: เราเพิ่งเปลี่ยนการจัดกลุ่มของวงเล็บ!

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

บางครั้งมันก็สามารถทำได้อย่างมีประสิทธิภาพ คอมไพเลอร์อาจสามารถปรับโครงสร้างรายการข้อมูลในหน่วยความจำให้เหมาะสม ในทางทฤษฎีมันมีข้อมูลเพียงพอที่รวบรวมเวลาที่จะคิดออกก็ควรทำได้ที่นี่: [x]เป็นเดี่ยวจึงเป็นเช่นเดียวกับ[x]<>xs cons x xsการทำซ้ำแต่ละครั้งของฟังก์ชั่นอาจจะสามารถใช้กรอบสแต็คเดียวกันอีกครั้งและอัปเดตพารามิเตอร์ให้เข้าที่

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

ดังนั้นอย่างที่คุณเห็นมันเป็นไปได้ที่จะปรับออเดอร์โมดูโลแบบเรียกซ้ำแบบหางโดยอัตโนมัติ semigroup ใด ๆ (ตัวอย่างหนึ่งซึ่งเป็นประเภทตัวเลขใด ๆ ที่ปกติภายใต้การคูณ) เป็นการพับแบบขี้เกียจขวา Haskell

สรุปเพิ่มเติม

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

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

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

times :: Monoid a => a -> Word -> a
times _ 0 = mempty
times x 1 = x
times x n | even n    = y <> y
          | otherwise = x <> y <> y
  where y = times x (n `quot` 2)

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


1
เราสามารถทำการทดลองเพื่อทดสอบว่าการเชื่อมโยงเป็นกุญแจสำคัญต่อความสามารถของ GCC ในการเพิ่มประสิทธิภาพนี้: pow(float x, unsigned n)รุ่นgcc.godbolt.org/z/eqwineจะปรับให้เหมาะสมเท่านั้นด้วย-ffast-math(ซึ่งหมายถึง-fassociative-mathจุดลอยตัวที่เข้มงวดนั้นไม่สัมพันธ์กันเนื่องจากมีขมับต่างกัน = การปัดเศษที่แตกต่างกัน) การแนะนำ1.0f * xที่ไม่ได้อยู่ในเครื่องนามธรรม C (แต่จะให้ผลเหมือนกันเสมอ) จากนั้นการคูณ n-1 do{res*=x;}while(--n!=1)จะเหมือนกับการเรียกซ้ำดังนั้นนี่คือการเพิ่มประสิทธิภาพที่ไม่ได้รับ
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.