Haskell: รายการ, อาร์เรย์, เวกเตอร์, ลำดับ


230

ฉันกำลังเรียนรู้ Haskell และอ่านบทความสองสามข้อเกี่ยวกับประสิทธิภาพที่แตกต่างของรายการ Haskell และ (แทรกภาษาของคุณ)

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

ใครช่วยอธิบายความแตกต่างระหว่างรายการ, อาร์เรย์, เวกเตอร์, ลำดับโดยไม่ต้องลึกมากในทฤษฎีวิทยาศาสตร์คอมพิวเตอร์ของโครงสร้างข้อมูล?

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

มีโครงสร้างข้อมูลอื่น ๆ ที่ฉันขาดหายไปและอาจมีประโยชน์หรือไม่


1
ดูที่คำตอบนี้เกี่ยวกับรายการ vs arrays: stackoverflow.com/questions/8196667/haskell-arrays-vs-lists Vectors ส่วนใหญ่มีประสิทธิภาพเหมือนกับอาร์เรย์ แต่ API ที่ใหญ่กว่า
Grzegorz Chrupała

จะดีที่ได้เห็น Data.Map พูดถึงที่นี่ด้วย ดูเหมือนว่าโครงสร้างข้อมูลที่มีประโยชน์โดยเฉพาะอย่างยิ่งสำหรับข้อมูลหลายมิติ
Martin Capodici

คำตอบ:


339

รายการร็อค

โครงสร้างข้อมูลที่เป็นมิตรมากที่สุดสำหรับข้อมูลตามลำดับใน Haskell คือรายการ

 data [a] = a:[a] | []

รายการให้ cons (1) ข้อเสียและการจับคู่รูปแบบ ห้องสมุดมาตรฐานและที่สำคัญโหมโรง, เต็มไปด้วยฟังก์ชั่นรายการที่มีประโยชน์ที่ควรครอกรหัสของคุณ ( foldr, map, filter) รายการมีความคงทนถาวรหรือใช้งานได้ดีซึ่งดีมาก รายการ Haskell ไม่ใช่ "รายการ" จริง ๆ เพราะเป็นแบบเหรียญ (ภาษาอื่นเรียกว่าสตรีมเหล่านี้) ดังนั้นสิ่งที่ต้องการ

ones :: [Integer]
ones = 1:ones

twos = map (+1) ones

tenTwos = take 10 twos

ทำงานอย่างยอดเยี่ยม โครงสร้างข้อมูลที่ไม่มีที่สิ้นสุดร็อค

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

ในทางกลับกัน

ปัญหาแรกของรายการคือการทำดัชนีให้(!!)ใช้เวลา ϴ (k) ซึ่งน่ารำคาญ ยิ่งไปกว่านั้นการผนวกอาจช้า++แต่โมเดลการประเมินผลที่ขี้เกียจของ Haskell หมายความว่าสิ่งเหล่านี้สามารถได้รับการตัดจำหน่ายอย่างสมบูรณ์หากเกิดขึ้น

ปัญหาที่สองกับรายการคือพวกเขามีตำแหน่งข้อมูลไม่ดี ตัวประมวลผลที่แท้จริงจะมีค่าคงที่สูงเมื่อวัตถุในหน่วยความจำไม่ถูกจัดวางติดกัน ดังนั้นใน C ++ std::vectorจะมี "snoc" ที่เร็วกว่า (วางวัตถุในตอนท้าย) กว่าโครงสร้างข้อมูลลิสต์ลิงก์ที่ฉันรู้จักแม้ว่านี่จะไม่ใช่โครงสร้างข้อมูลแบบถาวรดังนั้นจึงเป็นมิตรน้อยกว่าลิสต์ของ Haskell

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

ลำดับมีการทำงาน

Data.Sequenceขึ้นอยู่กับต้นไม้ภายใน (ฉันรู้ว่าคุณไม่ต้องการที่จะรู้สิ่งนี้) ซึ่งหมายความว่าพวกเขามีคุณสมบัติที่ดีบางอย่าง

  1. ทำงานได้อย่างหมดจด Data.Sequenceเป็นโครงสร้างข้อมูลถาวร
  2. เข้าถึงอย่างรวดเร็วไปยังจุดเริ่มต้นและจุดสิ้นสุดของต้นไม้ ϴ (1) (ตัดจำหน่าย) เพื่อรับองค์ประกอบแรกหรือสุดท้ายหรือต่อท้ายต้นไม้ ที่รายการสิ่งที่เร็วที่สุดData.Sequenceคืออย่างช้าที่สุดคงที่
  3. access (บันทึก n) เข้าถึงตรงกลางของลำดับ ซึ่งรวมถึงการแทรกค่าเพื่อสร้างลำดับใหม่
  4. API คุณภาพสูง

ในทางกลับกันData.Sequenceไม่ได้ทำอะไรมากสำหรับปัญหาในพื้นที่ข้อมูลและใช้งานได้กับการรวบรวมแบบ จำกัด เท่านั้น (ขี้เกียจน้อยกว่ารายการ)

อาร์เรย์ไม่เหมาะสำหรับคนที่ใจหดหู่

อาร์เรย์เป็นหนึ่งในโครงสร้างข้อมูลที่สำคัญที่สุดใน CS แต่มันไม่เหมาะกับโลกการทำงานอันแสนขี้เกียจ อาร์เรย์ให้การเข้าถึง middle (1) ตรงกลางของการรวบรวมและตำแหน่งข้อมูล / ปัจจัยคงที่ที่ดีเป็นพิเศษ แต่เนื่องจากพวกเขาไม่เหมาะกับ Haskell มากพวกเขาจึงเจ็บปวดที่จะใช้ จริงๆแล้วมีประเภทอาร์เรย์ที่แตกต่างกันจำนวนมากในไลบรารีมาตรฐานปัจจุบัน สิ่งเหล่านี้รวมถึงอาร์เรย์ที่คงอยู่อย่างถาวร, อาร์เรย์ที่ไม่แน่นอนสำหรับ IO monad, อาร์เรย์ที่ไม่แน่นอนสำหรับ ST monad และรุ่นที่ไม่ได้บรรจุข้างต้น ดูรายละเอียดเพิ่มเติมได้ที่ haskell wiki

เวกเตอร์เป็นอาร์เรย์ที่ "ดีกว่า"

Data.Vectorแพคเกจให้บริการทั้งหมดของความดีอาร์เรย์ในระดับสูงและการทำความสะอาด API ที่สูงขึ้น หากคุณไม่ทราบว่ากำลังทำอะไรอยู่คุณควรใช้สิ่งเหล่านี้หากคุณต้องการอาเรย์เช่นประสิทธิภาพ แน่นอนยังมีข้อแม้บางประการ - อาเรย์ที่เปลี่ยนแปลงไม่ได้เช่นโครงสร้างข้อมูลไม่เล่นดีในภาษาขี้เกียจ อย่างไรก็ตามบางครั้งคุณต้องการประสิทธิภาพ O (1) และ Data.Vectorมอบให้คุณในแพ็คเกจที่ใช้งานได้

คุณมีตัวเลือกอื่น ๆ

หากคุณต้องการรายการที่มีความสามารถในการแทรกอย่างมีประสิทธิภาพในตอนท้ายคุณสามารถใช้รายการที่แตกต่างได้ ตัวอย่างที่ดีที่สุดของรายการกวดขันขึ้นประสิทธิภาพการทำงานมีแนวโน้มที่จะมาจากการ[Char]ที่โหมโรงได้ aliased Stringเป็น Charรายการมีความสะดวก แต่มีแนวโน้มที่จะรันตามลำดับช้ากว่าสตริง C ถึง 20 เท่าดังนั้นอย่าลังเลที่จะใช้งานData.Textหรือเร็วมาก Data.ByteStringฉันแน่ใจว่ามีห้องสมุดที่มุ่งเน้นลำดับอื่น ๆ ที่ฉันไม่ได้คิดตอนนี้

ข้อสรุป

90 +% ของเวลาที่ฉันต้องการการรวบรวมตามลำดับในรายการ Haskell เป็นโครงสร้างข้อมูลที่ถูกต้อง รายการนั้นเหมือนตัววนซ้ำฟังก์ชันที่ใช้รายการสามารถใช้กับโครงสร้างข้อมูลอื่น ๆ เหล่านี้ได้อย่างง่ายดายโดยใช้toListฟังก์ชันที่มาพร้อมกับ ในโลกที่ดีกว่าการโหมโรงจะเป็นตัวแปรอย่างสมบูรณ์เกี่ยวกับประเภทของคอนเทนเนอร์ที่ใช้ แต่ในปัจจุบัน []litters ห้องสมุดมาตรฐาน ดังนั้นการใช้รายการ (เกือบ) ทุกที่ไม่เป็นไรแน่นอน
คุณสามารถรับฟังก์ชั่นรายการส่วนใหญ่ (และมีเกียรติที่จะใช้พวกเขา)

Prelude.map                --->  Prelude.fmap (works for every Functor)
Prelude.foldr/foldl/etc    --->  Data.Foldable.foldr/foldl/etc
Prelude.sequence           --->  Data.Traversable.sequence
etc

ในความเป็นจริงData.Traversableกำหนด API ที่เป็นสากลมากขึ้นหรือน้อยลงในรายการ "สิ่งใด ๆ "

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


แก้ไข: ขึ้นอยู่กับความเห็นของฉันรู้ฉันไม่เคยอธิบายเมื่อจะใช้VSData.Vector Data.Sequenceอาร์เรย์และเวกเตอร์ให้การจัดทำดัชนีและการแบ่งส่วนข้อมูลได้อย่างรวดเร็วที่สุด แต่มีโครงสร้างข้อมูลชั่วคราว (จำเป็น) พื้นฐาน โครงสร้างข้อมูลที่สามารถใช้งานได้จริงเช่นData.Sequenceและ[]ปล่อยให้สร้างคุณค่าใหม่จากค่าเก่าราวกับว่าคุณได้แก้ไขค่าเก่า

  newList oldList = 7 : drop 5 oldList

ไม่แก้ไขรายการเก่าและไม่ต้องคัดลอก ดังนั้นแม้ว่าoldListจะยาวอย่างไม่น่าเชื่อ "การปรับเปลี่ยน" นี้จะเร็วมาก เหมือนกับ

  newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence 

จะสร้างลำดับใหม่โดยมีnewValueการแทนที่สำหรับองค์ประกอบ 3000 รายการ อีกครั้งมันไม่ทำลายลำดับเก่า แต่เพิ่งสร้างใหม่ แต่การทำเช่นนี้มีประสิทธิภาพมากโดยใช้ O (log (min (k, kn)) โดยที่ n คือความยาวของลำดับและ k คือดัชนีที่คุณปรับเปลี่ยน

คุณลาดเทได้อย่างง่ายดายทำเช่นนี้กับและVectors Arraysพวกเขาสามารถแก้ไขได้แต่นั่นคือการแก้ไขที่จำเป็นจริงและไม่สามารถทำได้ในรหัส Haskell ปกติ นั่นหมายถึงการดำเนินการในVectorแพ็คเกจที่ทำการปรับเปลี่ยนเช่นsnocและconsต้องคัดลอกทั้งเวกเตอร์เพื่อใช้O(n)เวลา ข้อยกเว้นเพียงอย่างเดียวคือคุณสามารถใช้เวอร์ชันที่ไม่แน่นอน ( Vector.Mutable) ภายในSTmonad (หรือIO) และทำการแก้ไขทั้งหมดของคุณเช่นเดียวกับที่คุณต้องการในภาษาที่จำเป็น เมื่อเสร็จแล้วคุณ "หยุด" เวกเตอร์ของคุณเพื่อเปลี่ยนเป็นโครงสร้างที่ไม่เปลี่ยนรูปแบบที่คุณต้องการใช้กับรหัสบริสุทธิ์

ความรู้สึกของฉันคือคุณควรใช้ค่าเริ่มต้นData.Sequenceหากรายการไม่เหมาะสม ใช้Data.Vectorเฉพาะเมื่อรูปแบบการใช้งานของคุณไม่เกี่ยวข้องกับการปรับเปลี่ยนมากมายหรือถ้าคุณต้องการประสิทธิภาพที่สูงมากภายใน Monads ST / IO

ถ้าหากการพูดคุยของนี้STmonad จะออกจากคุณสับสน: Data.Sequenceทั้งหมดเป็นเหตุผลที่ต้องติดได้อย่างรวดเร็วบริสุทธิ์และสวยงาม


45
ความเข้าใจอย่างหนึ่งที่ฉันได้ยินคือรายการโดยทั่วไปมีโครงสร้างการควบคุมมากเท่ากับโครงสร้างข้อมูลใน Haskell และนี่ก็สมเหตุสมผล: ที่ที่คุณจะใช้ C-style ในการวนซ้ำในภาษาอื่นคุณจะใช้[1..]รายการใน Haskell รายการสามารถใช้เพื่อความสนุกสนานเช่นการย้อนรอย การคิดเกี่ยวกับพวกเขาเป็นโครงสร้างควบคุม (เรียงลำดับ) ช่วยให้เข้าใจถึงวิธีการใช้งานจริง ๆ
Tikhon Jelvis

21
คำตอบที่ยอดเยี่ยม ข้อร้องเรียนของฉันเพียงอย่างเดียวคือ ลำดับเป็น awesomesauce ทำงาน อีกหนึ่งโบนัสสำหรับพวกเขาคือการเข้าร่วมและการแยกอย่างรวดเร็ว (บันทึก n)
Dan Burton

3
@DanBurton Fair Data.Sequenceฉันไม่อาจจะกดราคา ต้นไม้ลายนิ้วมือเป็นหนึ่งในสิ่งประดิษฐ์ที่Data.Sequenceยอดเยี่ยมที่สุดในประวัติศาสตร์ของการคำนวณ (Guibas น่าจะได้รับรางวัลทัวริงสักวัน) และเป็นการใช้งานที่ยอดเยี่ยมและมี API ที่ใช้งานได้ดีมาก
ฟิลิปเจเอฟ

3
"UseData.Vector เฉพาะในกรณีที่รูปแบบการใช้งานของคุณไม่ได้เกี่ยวข้องกับการแก้ไขจำนวนมากหรือหากคุณต้องการประสิทธิภาพการทำงานสูงมากภายใน monads ST / IO .." ถ้อยคำที่น่าสนใจเพราะถ้าคุณจะทำให้การปรับเปลี่ยนจำนวนมาก (เช่นซ้ำ ๆ (100k ครั้ง) การพัฒนาองค์ประกอบ 100k) แล้วคุณจะทำจำเป็น ST / IO เวกเตอร์เพื่อให้ได้ประสิทธิภาพที่ยอมรับ
misterbee

4
ความกังวลเกี่ยวกับเวกเตอร์และการคัดลอก (บริสุทธิ์) บางส่วนได้รับการบรรเทาโดยฟิวชั่นสตรีมเช่นนี้: import qualified Data.Vector.Unboxed as VU; main = print (VU.cons 'a' (VU.replicate 100 'b'))รวบรวมการจัดสรร 404 ไบต์ (101 ตัวอักษร) ใน Core: hpaste.org/65015
FunctorSalad
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.