ความfold
แตกต่างนั้นดูเหมือนจะเป็นสาเหตุของความสับสนอยู่บ่อยครั้งดังนั้นนี่คือภาพรวมทั่วไป:
พิจารณาพับรายการของค่าเอ็น[x1, x2, x3, x4 ... xn ]
ที่มีฟังก์ชั่นบางอย่างและเมล็ดf
z
foldl
คือ:
- การเชื่อมโยงด้านซ้าย :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- หางแบบวนซ้ำ : มันวนซ้ำผ่านรายการสร้างมูลค่าในภายหลัง
- ขี้เกียจ : ไม่มีการประเมินผลจนกว่าจะต้องการผลลัพธ์
- ย้อนกลับ : ย้อน
foldl (flip (:)) []
กลับรายการ
foldr
คือ:
- การเชื่อมโยงที่ถูกต้อง :
f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
- วนซ้ำเป็นอาร์กิวเมนต์ : การวนซ้ำแต่ละครั้งใช้
f
กับค่าถัดไปและผลของการพับส่วนที่เหลือของรายการ
- ขี้เกียจ : ไม่มีการประเมินผลจนกว่าจะต้องการผลลัพธ์
- ส่งต่อ :
foldr (:) []
ส่งคืนรายการที่ไม่เปลี่ยนแปลง
มีประเด็นที่ละเอียดอ่อนเล็กน้อยนี่ที่เดินทางคนขึ้นไปบางครั้งเพราะfoldl
เป็นข้างหลังแอพลิเคชันของแต่ละf
จะถูกเพิ่มในนอกของผล; และเนื่องจากขี้เกียจจึงไม่มีการประเมินผลใด ๆ จนกว่าจะได้ผลลัพธ์ที่ต้องการ ซึ่งหมายความว่าในการคำนวณส่วนใดส่วนหนึ่งของผลลัพธ์ก่อนอื่น Haskell จะวนซ้ำผ่านรายการทั้งหมดโดยสร้างนิพจน์ของแอปพลิเคชันฟังก์ชันที่ซ้อนกันจากนั้นประเมินฟังก์ชันด้านนอกสุดประเมินอาร์กิวเมนต์ตามต้องการ ถ้าf
มักจะใช้อาร์กิวเมนต์แรกนี้หมายถึง Haskell มีการ recurse f
ทุกทางลงไปที่ระยะสุดแล้วทำงานย้อนกลับคอมพิวเตอร์แอพลิเคชันของแต่ละ
เห็นได้ชัดว่านี่เป็นหนทางไกลจากโปรแกรมเมอร์ที่ใช้งานได้อย่างมีประสิทธิภาพส่วนใหญ่รู้จักและชื่นชอบ!
ในความเป็นจริงแม้ว่าจะfoldl
เป็นแบบหางซ้ำในทางเทคนิคเนื่องจากนิพจน์ผลลัพธ์ทั้งหมดถูกสร้างขึ้นก่อนที่จะประเมินสิ่งใดก็ตามfoldl
อาจทำให้เกิดสแต็กล้น!
บนมืออื่น ๆ foldr
ให้พิจารณา มันขี้เกียจเช่นกัน แต่เนื่องจากมันวิ่งไปข้างหน้าแอพพลิเคชั่นแต่ละตัวf
จะถูกเพิ่มเข้าไปด้านในของผลลัพธ์ ดังนั้นในการคำนวณผลลัพธ์ Haskell จึงสร้างแอปพลิเคชันฟังก์ชันเดียวอาร์กิวเมนต์ที่สองซึ่งเป็นส่วนที่เหลือของรายการที่พับไว้ ถ้าf
ขี้เกียจในอาร์กิวเมนต์ที่สองเช่นตัวสร้างข้อมูล - ผลลัพธ์จะขี้เกียจเพิ่มขึ้นโดยแต่ละขั้นตอนของการพับจะคำนวณเฉพาะเมื่อมีการประเมินบางส่วนของผลลัพธ์ที่ต้องการเท่านั้น
ดังนั้นเราจึงสามารถเห็นได้ว่าทำไมfoldr
บางครั้งจึงทำงานในรายการที่ไม่มีที่สิ้นสุดเมื่อfoldl
ไม่ได้: อดีตสามารถแปลงรายการที่ไม่มีที่สิ้นสุดให้เป็นโครงสร้างข้อมูลที่ไม่มีที่สิ้นสุดที่ขี้เกียจอื่นได้อย่างเกียจคร้านในขณะที่รายการหลังต้องตรวจสอบรายการทั้งหมดเพื่อสร้างส่วนใดส่วนหนึ่งของผลลัพธ์ ในทางกลับกันfoldr
ด้วยฟังก์ชันที่ต้องการอาร์กิวเมนต์ทันทีเช่นใช้(+)
งานได้ (หรือค่อนข้างไม่ได้ผล) foldl
ให้สร้างนิพจน์ขนาดใหญ่ก่อนที่จะประเมิน
ดังนั้นสองประเด็นสำคัญที่ควรทราบมีดังนี้:
foldr
สามารถเปลี่ยนโครงสร้างข้อมูลที่เรียกซ้ำแบบขี้เกียจไปเป็นอีกโครงสร้างหนึ่งได้
- มิฉะนั้นการพับแบบขี้เกียจจะผิดพลาดเมื่อมีสแต็กล้นในรายการขนาดใหญ่หรือไม่สิ้นสุด
คุณอาจสังเกตเห็นว่าดูเหมือนfoldr
ทำได้ทุกอย่างfoldl
และอื่น ๆ อีกมากมาย นี่คือเรื่องจริง! ในความเป็นจริงพับเกือบจะไม่มีประโยชน์!
แต่ถ้าเราต้องการสร้างผลลัพธ์ที่ไม่ขี้เกียจโดยการพับรายการขนาดใหญ่ (แต่ไม่สิ้นสุด) ล่ะ? สำหรับสิ่งนี้เราต้องการการพับที่เข้มงวดซึ่งแม้ว่าไลบรารีมาตรฐานจะมีให้ :
foldl'
คือ:
- การเชื่อมโยงด้านซ้าย :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- หางแบบวนซ้ำ : มันวนซ้ำผ่านรายการสร้างมูลค่าในภายหลัง
- เข้มงวด : แอปพลิเคชันฟังก์ชันแต่ละตัวจะได้รับการประเมินระหว่างทาง
- ย้อนกลับ : ย้อน
foldl' (flip (:)) []
กลับรายการ
เนื่องจากfoldl'
มีความเข้มงวดในการคำนวณผลลัพธ์ Haskell จะประเมิน f
ในแต่ละขั้นตอนแทนที่จะปล่อยให้อาร์กิวเมนต์ด้านซ้ายสะสมนิพจน์ขนาดใหญ่ที่ไม่มีการประเมิน สิ่งนี้ทำให้เรามีการเรียกหางซ้ำตามปกติและมีประสิทธิภาพที่เราต้องการ! กล่าวอีกนัยหนึ่ง:
foldl'
สามารถพับรายการขนาดใหญ่ได้อย่างมีประสิทธิภาพ
foldl'
จะค้างอยู่ในลูปที่ไม่มีที่สิ้นสุด (ไม่ทำให้สแตกล้น) ในรายการที่ไม่มีที่สิ้นสุด
Haskell wiki มีหน้าที่พูดถึงเรื่องนี้เช่นกัน
foldr
ดีกว่าfoldl
ในHaskellในขณะที่ตรงกันข้ามเป็นจริงในErlang (ซึ่งฉันเรียนรู้มาก่อนHaskell ) เนื่องจากErlangไม่ใช่คนขี้เกียจและฟังก์ชั่นก็ไม่ได้โค้งงอดังนั้นfoldl
ในErlang จึงมีพฤติกรรมเหมือนfoldl'
ข้างบน นี่ตอบโจทย์มาก! ทำได้ดีมากและขอบคุณ!