ตัวตรวจสอบประเภทของ Haskell นั้นสมเหตุสมผล ปัญหาคือผู้เขียนห้องสมุดที่คุณใช้อยู่ได้ทำบางสิ่งบางอย่าง ... ไม่สมเหตุสมผล
คำตอบสั้น ๆ คือ: ใช่เป็นที่ถูกต้องสมบูรณ์ถ้ามีอินสแตนซ์10 :: (Float, Float) Num (Float, Float)ไม่มีอะไร "ผิดมาก" เกี่ยวกับเรื่องนี้จากมุมมองของคอมไพเลอร์หรือภาษา มันไม่ได้กำลังสองตามสัญชาตญาณของเราเกี่ยวกับสิ่งที่ตัวอักษรตัวเลขทำ เนื่องจากคุณคุ้นเคยกับระบบประเภทที่ตรวจจับข้อผิดพลาดที่คุณทำคุณรู้สึกประหลาดใจและผิดหวังอย่างแท้จริง!
Numอินสแตนซ์และfromIntegerปัญหา
คุณประหลาดใจว่าคอมไพเลอร์ยอมรับคือ10 :: Coord 10 :: (Float, Float)เป็นเรื่องสมเหตุสมผลที่จะสมมติว่าตัวอักษรตัวเลขเช่น10จะอนุมานได้ว่ามีประเภท "ตัวเลข" ออกจากกล่องอักษรตัวเลขสามารถตีความได้ว่าInt, Integer, หรือFloat Doubleตัวเลขสองตัวที่ไม่มีบริบทอื่นดูเหมือนตัวเลขทั้งสี่ประเภทนี้เป็นตัวเลข Complexเราไม่ได้พูดคุยเกี่ยวกับ
โชคดีหรือโชคไม่ดีอย่างไรก็ตาม Haskell เป็นภาษาที่ยืดหยุ่นมาก ระบุมาตรฐานที่แท้จริงจำนวนเต็มเหมือน10จะถูกตีความว่าเป็นซึ่งมีประเภทfromInteger 10 Num a => aดังนั้น10อาจอนุมานได้ว่าเป็นประเภทใดก็ตามที่มีNumอินสแตนซ์เขียนไว้ ฉันอธิบายในรายละเอียดอีกเล็กน้อยในคำตอบอื่น
ดังนั้นเมื่อคุณโพสต์คำถามของคุณที่มีประสบการณ์ Haskeller เห็นได้ทันทีว่าสำหรับการ10 :: (Float, Float)ได้รับการยอมรับจะต้องมีตัวอย่างเช่นหรือNum a => Num (a, a) Num (Float, Float)ไม่มีอินสแตนซ์ดังกล่าวในPreludeดังนั้นจึงต้องกำหนดไว้ที่อื่น เมื่อใช้:i Numคุณจะเห็นได้อย่างรวดเร็วว่ามาจากที่ใด: glossแพ็คเกจ
พิมพ์คำพ้องความหมายและอินสแตนซ์ orphan
แต่รอสักครู่ คุณไม่ได้ใช้glossประเภทใด ๆในตัวอย่างนี้ เหตุใดอินสแตนซ์จึงglossส่งผลต่อคุณ คำตอบมีสองขั้นตอน
ครั้งแรกไวพจน์ประเภทการแนะนำให้รู้จักกับคำหลักtypeไม่สร้างรูปแบบใหม่ ในโมดูลของคุณเขียนเป็นเพียงชวเลขCoord (Float, Float)ในทำนองเดียวกันในGraphics.Gloss.Data.Point, วิธีการPoint (Float, Float)กล่าวอีกนัยหนึ่งของคุณCoordและglossของคุณPointเทียบเท่ากันอย่างแท้จริง
ดังนั้นเมื่อผู้glossดูแลเลือกที่จะเขียนinstance Num Point where ...พวกเขาก็ทำให้Coordประเภทของคุณเป็นตัวอย่างเช่นNumกัน นั่นคือเทียบเท่ากับหรือinstance Num (Float, Float) where ...instance Num Coord where ...
(โดยค่าเริ่มต้น Haskell ไม่อนุญาตให้คำพ้องความหมายประเภทเป็นอินสแตนซ์คลาสglossผู้เขียนต้องเปิดใช้งานส่วนขยายภาษาTypeSynonymInstancesและFlexibleInstancesเขียนอินสแตนซ์)
ประการที่สองนี้เป็นที่น่าแปลกใจเพราะมันเป็นเช่นเด็กกำพร้าคือการประกาศเช่นinstance C Aที่ทั้งสองCและAมีการกำหนดในโมดูลอื่น ๆ นี่มันร้ายกาจอย่างยิ่งเพราะแต่ละส่วนที่เกี่ยวข้องเช่นNum, (,)และFloatมาจากPreludeและมีแนวโน้มที่จะอยู่ในขอบเขตทุกที่
ความคาดหวังของคุณคือการที่Numกำหนดไว้ในPreludeและ tuples และFloatมีการกำหนดในเพื่อให้ทุกอย่างเกี่ยวกับวิธีการทำงานของผู้ที่สามสิ่งที่ถูกกำหนดไว้ในPrelude Preludeเหตุใดการนำเข้าโมดูลที่แตกต่างไปจากเดิมอย่างสิ้นเชิงจึงเปลี่ยนแปลงอะไร ๆ ตามหลักการแล้วมันจะไม่เกิดขึ้น แต่อินสแตนซ์เด็กกำพร้าทำลายสัญชาตญาณนั้น
(โปรดทราบว่า GHC เตือนเกี่ยวกับอินสแตนซ์เด็กกำพร้า - ผู้เขียนglossลบล้างคำเตือนนั้นโดยเฉพาะซึ่งควรขึ้นธงสีแดงและแจ้งเตือนอย่างน้อยในเอกสารประกอบ)
อินสแตนซ์ของคลาสเป็นแบบส่วนกลางและไม่สามารถซ่อนได้
นอกจากนี้อินสแตนซ์คลาสยังเป็นแบบโกลบอล : อินสแตนซ์ใด ๆ ที่กำหนดในโมดูลใด ๆ ที่นำเข้าจากโมดูลของคุณจะอยู่ในบริบทและพร้อมใช้งานสำหรับตัวตรวจสอบตัวพิมพ์เมื่อทำการแก้ไขอินสแตนซ์ สิ่งนี้ทำให้การให้เหตุผลทั่วโลกเป็นเรื่องสะดวกเพราะเราสามารถ (โดยปกติ) สมมติว่าฟังก์ชันคลาสเช่น(+)จะเหมือนกันสำหรับประเภทที่กำหนดเสมอ อย่างไรก็ตามยังหมายความว่าการตัดสินใจในระดับท้องถิ่นมีผลกระทบทั่วโลก การกำหนดอินสแตนซ์คลาสจะเปลี่ยนบริบทของโค้ดดาวน์สตรีมโดยไม่สามารถเพิกถอนได้โดยไม่มีวิธีใดที่จะปิดบังหรือซ่อนมันไว้เบื้องหลังขอบเขตของโมดูล
คุณไม่สามารถใช้รายการนำเข้าเพื่อหลีกเลี่ยงการนำเข้ากรณี ในทำนองเดียวกันคุณไม่สามารถหลีกเลี่ยงการส่งออกอินสแตนซ์จากโมดูลที่คุณกำหนดได้
นี่เป็นประเด็นที่มีปัญหาและมีการพูดถึงกันมากในการออกแบบภาษา Haskell มีการอภิปรายที่น่าสนใจเกี่ยวกับปัญหาที่เกี่ยวข้องในเธรด Redditนี้ ตัวอย่างเช่นดูความคิดเห็นของ Edward Kmett เกี่ยวกับการอนุญาตให้มีการควบคุมการมองเห็นสำหรับอินสแตนซ์: "โดยพื้นฐานแล้วคุณจะโยนความถูกต้องของโค้ดเกือบทั้งหมดที่ฉันเขียนออกไป"
(อย่างไรก็ตามดังที่คำตอบนี้แสดงให้เห็นคุณสามารถทำลายสมมติฐานอินสแตนซ์ทั่วโลกได้ในบางประเด็นโดยใช้อินสแตนซ์เด็กกำพร้า!)
สิ่งที่ต้องทำ - สำหรับผู้ติดตั้งไลบรารี
Numคิดว่าสองครั้งก่อนที่จะดำเนินการ คุณไม่สามารถfromIntegerแก้ไขปัญหาได้ - ไม่การกำหนดfromInteger = error "not implemented"ไม่ได้ทำให้ดีขึ้น ผู้ใช้ของคุณจะสับสนหรือประหลาดใจหรือแย่กว่านั้นคือไม่เคยสังเกตเห็นว่าตัวอักษรจำนวนเต็มถูกอนุมานโดยบังเอิญว่ามีประเภทที่คุณกำลังสร้างอินสแตนซ์หรือไม่ คือการให้(*)และ(+)ที่สำคัญโดยเฉพาะอย่างยิ่งถ้าคุณมีการตัดมันได้หรือไม่
พิจารณาใช้ตัวดำเนินการทางคณิตศาสตร์ทางเลือกที่กำหนดไว้ในไลบรารีเช่น Conal Elliott's vector-space(สำหรับชนิดของชนิด*) หรือ Edward Kmett's linear(สำหรับประเภทของชนิด* -> *) นี่คือสิ่งที่ฉันมักจะทำด้วยตัวเอง
ใช้-Wall. อย่าใช้อินสแตนซ์ orphan และอย่าปิดใช้งานคำเตือนอินสแตนซ์ orphan
อีกวิธีหนึ่งตามนำของlinearห้องสมุดและมีความประพฤติดีอื่น ๆ อีกมากมายและให้เด็กกำพร้าอินสแตนซ์ในโมดูลที่แยกต่างหากสิ้นสุดในหรือ.OrphanInstances .Instancesและไม่ได้นำเข้าโมดูลที่จากโมดูลอื่น ๆ จากนั้นผู้ใช้สามารถนำเข้าเด็กกำพร้าได้อย่างชัดเจนหากต้องการ
หากคุณพบว่าตัวเองกำลังกำหนดเด็กกำพร้าให้พิจารณาขอให้ผู้ดูแลต้นน้ำดำเนินการแทนหากเป็นไปได้และเหมาะสม ผมเคยเขียนบ่อยเช่นเด็กกำพร้าจนกว่าพวกเขาจะเพิ่มไปยังShow a => Show (Identity a) transformersฉันอาจจะรายงานข้อบกพร่องเกี่ยวกับเรื่องนี้ด้วยซ้ำ ฉันจำไม่ได้
สิ่งที่ต้องทำ - สำหรับผู้ใช้ห้องสมุด
คุณไม่มีตัวเลือกมากมาย ติดต่อเจ้าหน้าที่ดูแลห้องสมุดอย่างสุภาพและสร้างสรรค์! - ชี้ให้พวกเขาไปที่คำถามนี้ พวกเขาอาจมีเหตุผลพิเศษบางอย่างในการเขียนเด็กกำพร้าที่มีปัญหาหรืออาจไม่รู้ตัว
ในวงกว้างมากขึ้น: ตระหนักถึงความเป็นไปได้นี้ นี่เป็นหนึ่งในไม่กี่พื้นที่ของ Haskell ที่มีผลกระทบระดับโลกอย่างแท้จริง คุณต้องตรวจสอบว่าทุกโมดูลที่คุณนำเข้าและทุกโมดูลที่นำเข้าโมดูลเหล่านั้นไม่ได้ใช้อินสแตนซ์ orphan คำอธิบายประกอบประเภทบางครั้งอาจแจ้งเตือนคุณถึงปัญหาและแน่นอนคุณสามารถใช้:iใน GHCi เพื่อตรวจสอบ
กำหนดnewtypes ของคุณเองแทนtypeคำเหมือนถ้ามันสำคัญพอ คุณค่อนข้างมั่นใจได้ว่าจะไม่มีใครยุ่งกับพวกเขา
หากคุณมีปัญหาบ่อยครั้งที่เกิดจากไลบรารีโอเพนซอร์สแน่นอนว่าคุณสามารถสร้างไลบรารีเวอร์ชันของคุณเองได้ แต่การดูแลรักษาอาจทำให้ปวดหัวได้อย่างรวดเร็ว