คุณรู้ได้อย่างไรว่าเมื่อใดควรใช้พับซ้ายและเมื่อใดควรใช้พับขวา?


100

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

สิ่งที่ฉันอยากรู้คือ:

  • มีกฎอะไรบ้างในการพิจารณาว่าจะพับซ้ายหรือพับขวา?
  • ฉันจะตัดสินใจได้อย่างไรว่าจะใช้การพับแบบใดในขณะที่ประสบปัญหา

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

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


1
มีลักษณะคล้ายกับstackoverflow.com/questions/384797/…
Steven Huwig

1
คำถามนี้เป็นเรื่องทั่วไปมากกว่าซึ่งเกี่ยวกับ Haskell โดยเฉพาะ ความเกียจคร้านสร้างความแตกต่างอย่างมากในคำตอบสำหรับคำถาม
Chris Conway

โอ้. แปลกฉันคิดว่าฉันเห็นแท็ก Haskell ในคำถามนี้ แต่ฉันเดาว่าไม่ใช่ ...
ephemient

คำตอบ:


108

คุณสามารถโอนการพับเป็นสัญกรณ์ตัวดำเนินการ infix (เขียนระหว่าง):

ตัวอย่างนี้พับโดยใช้ฟังก์ชันตัวสะสม x

fold x [A, B, C, D]

จึงเท่ากับ

A x B x C x D

ตอนนี้คุณต้องให้เหตุผลเกี่ยวกับความเชื่อมโยงของตัวดำเนินการของคุณ (โดยใส่วงเล็บ!)

หากคุณมีตัวดำเนินการเชื่อมโยงด้านซ้ายคุณจะต้องกำหนดวงเล็บเช่นนี้

((A x B) x C) x D

ที่นี่คุณใช้พับด้านซ้าย ตัวอย่าง (รหัสเทียมแบบแฮสเคลล์)

foldl (-) [1, 2, 3] == (1 - 2) - 3 == 1 - 2 - 3 // - is left-associative

หากตัวดำเนินการของคุณเชื่อมโยงกันอย่างถูกต้อง ( พับขวา ) วงเล็บจะถูกตั้งค่าดังนี้:

A x (B x (C x D))

ตัวอย่าง: Cons-Operator

foldr (:) [] [1, 2, 3] == 1 : (2 : (3 : [])) == 1 : 2 : 3 : [] == [1, 2, 3]

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


6
สิ่งที่คุณอธิบายนั้นเป็นจริงfoldl1และfoldr1ใน Haskell ( foldlและfoldrใช้ค่าเริ่มต้นภายนอก) และ "ข้อเสีย" ของ Haskell เรียกว่า(:)not (::)แต่อย่างอื่นถูกต้อง คุณอาจต้องการเพิ่มว่า Haskell ยังมีfoldl'/ foldl1'ซึ่งเป็นตัวแปรที่เข้มงวดของfoldl/ foldl1เนื่องจากการคำนวณแบบเกียจคร้านไม่เป็นที่ต้องการเสมอไป
ephemient

ขอโทษค่ะฉันคิดว่าฉันเห็นแท็ก "Haskell" ในคำถามนี้ แต่ไม่มี ความคิดเห็นของฉันไม่สมเหตุสมผลเท่าไหร่ถ้าไม่ใช่ Haskell ...
ephemient

@ephemient คุณเห็นแล้ว มันคือ "รหัสเทียมแบบแฮสเซลล์" :)
laughing_man

คำตอบที่ดีที่สุดที่ฉันเคยเห็นเกี่ยวกับความแตกต่างระหว่างการพับ
AleXoundOS

61

Olin Shivers ทำให้พวกเขาแตกต่างโดยพูดว่า "foldl เป็นตัวทำซ้ำรายการพื้นฐาน" และ "foldr คือตัวดำเนินการเรียกซ้ำรายการพื้นฐาน" หากคุณดูวิธีการทำงานของพับ:

((1 + 2) + 3) + 4

คุณสามารถเห็นตัวสะสม (เช่นเดียวกับการวนซ้ำหางซ้ำ) ที่กำลังสร้างขึ้น ในทางตรงกันข้าม Foldr ดำเนินการ:

1 + (2 + (3 + 4))

ซึ่งคุณสามารถดูการส่งผ่านไปยังกรณีฐาน 4 และสร้างผลลัพธ์จากที่นั่น

ดังนั้นฉันจึงวางกฎง่ายๆ: ถ้ามันดูเหมือนการวนซ้ำรายการสิ่งที่ง่ายในการเขียนในรูปแบบหางซ้ำพับเป็นวิธีที่จะไป

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


29

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

A List(1,2,3).foldRight(0)(_ + _)จะลดเป็น:

1 + List(2,3).foldRight(0)(_ + _)        // first stack frame
    2 + List(3).foldRight(0)(_ + _)      // second stack frame
        3 + 0                            // third stack frame 
// (I don't remember if the JVM allocates space 
// on the stack for the third frame as well)

ในขณะที่List(1,2,3).foldLeft(0)(_ + _)จะลดเป็น:

(((0 + 1) + 2) + 3)

ซึ่งสามารถคำนวณซ้ำเช่นทำในการดำเนินงานของList

ในภาษาที่ได้รับการประเมินอย่างเคร่งครัดเช่น Scala foldRightสามารถระเบิดสแต็กสำหรับรายการขนาดใหญ่ได้อย่างง่ายดายในขณะที่foldLeftจะไม่

ตัวอย่าง:

scala> List.range(1, 10000).foldLeft(0)(_ + _)
res1: Int = 49995000

scala> List.range(1, 10000).foldRight(0)(_ + _)
java.lang.StackOverflowError
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRig...

ดังนั้นกฎทั่วไปของฉันคือ - สำหรับตัวดำเนินการที่ไม่มีการเชื่อมโยงที่เฉพาะเจาะจงให้ใช้เสมอfoldLeftอย่างน้อยก็ใน Scala มิฉะนั้นให้ไปพร้อมกับคำแนะนำอื่น ๆ ที่ให้ไว้ในคำตอบ;)


13
สิ่งนี้เป็นจริงก่อนหน้านี้ แต่ในเวอร์ชันปัจจุบันของ Scala นั้น foldRight ถูกเปลี่ยนเป็นใช้ foldLeft กับสำเนาที่กลับรายการของรายการ ยกตัวอย่างเช่นใน 2.10.3, github.com/scala/scala/blob/v2.10.3/src/library/scala/... ดูเหมือนว่าจะมีการเปลี่ยนแปลงนี้เมื่อต้นปี 2013 - github.com/scala/scala/commit/… .
Dhruv Kapoor

4

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

foldl: (((1 + 2) + 3) + 4)สามารถคำนวณแต่ละการดำเนินการและส่งต่อมูลค่าสะสมไปข้างหน้า

Foldr: (1 + (2 + (3 + 4)))ต้องการเปิดสแต็กเฟรมสำหรับ1 + ?และ2 + ?ก่อนที่จะคำนวณ3 + 4จากนั้นจะต้องย้อนกลับและทำการคำนวณสำหรับแต่ละรายการ

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


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

4
ลักษณะขี้เกียจของ Haskell ทำให้การวิเคราะห์นี้สับสน หากฟังก์ชันที่พับไม่เข้มงวดในพารามิเตอร์ที่สองก็foldrจะมีประสิทธิภาพมากกว่าfoldlและไม่ต้องใช้เฟรมสแต็กเพิ่มเติม
ephemient

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