ตัวตรวจสอบประเภทของ 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 เพื่อตรวจสอบ
กำหนดnewtype
s ของคุณเองแทนtype
คำเหมือนถ้ามันสำคัญพอ คุณค่อนข้างมั่นใจได้ว่าจะไม่มีใครยุ่งกับพวกเขา
หากคุณมีปัญหาบ่อยครั้งที่เกิดจากไลบรารีโอเพนซอร์สแน่นอนว่าคุณสามารถสร้างไลบรารีเวอร์ชันของคุณเองได้ แต่การดูแลรักษาอาจทำให้ปวดหัวได้อย่างรวดเร็ว