ตัวตรวจสอบประเภทอนุญาตให้มีการเปลี่ยนประเภทที่ไม่ถูกต้องและโปรแกรมยังคงคอมไพล์


100

ในขณะที่พยายามแก้ไขปัญหาในโปรแกรมของฉัน (วงกลม 2 วงที่มีรัศมีเท่ากันถูกวาดให้มีขนาดต่างกันโดยใช้กลอส*) ฉันเจอสถานการณ์แปลก ๆ ในไฟล์ของฉันที่จัดการวัตถุฉันมีคำจำกัดความต่อไปนี้สำหรับPlayer:

type Coord = (Float,Float)
data Obj =  Player  { oPos :: Coord, oDims :: Coord }

และในไฟล์หลักของฉันซึ่งนำเข้า Objects.hs ฉันมีคำจำกัดความดังต่อไปนี้:

startPlayer :: Obj
startPlayer = Player (0,0) 10

สิ่งนี้เกิดขึ้นเนื่องจากฉันเพิ่มและเปลี่ยนฟิลด์สำหรับผู้เล่นและลืมอัปเดตstartPlayerหลังจากนั้น (ขนาดของมันถูกกำหนดโดยตัวเลขเดียวเพื่อแสดงรัศมี แต่ฉันเปลี่ยนเป็น a Coordเพื่อแสดง (ความกว้างความสูง) ในกรณีที่ฉันเคยทำ ผู้เล่นวัตถุที่ไม่ใช่วงกลม)

สิ่งที่น่าทึ่งคือโค้ดด้านบนคอมไพล์และรันแม้ว่าฟิลด์ที่สองจะผิดประเภท

ตอนแรกฉันคิดว่าบางทีฉันอาจจะเปิดไฟล์เวอร์ชันต่างๆ แต่การเปลี่ยนแปลงใด ๆ กับไฟล์ใด ๆ จะแสดงในโปรแกรมที่คอมไพล์แล้ว

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

ฉันลองวางตัวอย่างข้อมูล 2 รายการด้านบนลงในไฟล์ของตัวเองและพบข้อผิดพลาดที่คาดว่าจะเกิดขึ้นซึ่งช่องที่สองของPlayerin startPlayerไม่ถูกต้อง

อะไรที่อาจทำให้สิ่งนี้เกิดขึ้นได้? คุณคิดว่านี่เป็นสิ่งที่ตัวตรวจสอบประเภทของ Haskell ควรป้องกัน


* คำตอบสำหรับปัญหาเดิมของฉันวงกลมสองวงที่มีรัศมีเท่ากันถูกวาดให้มีขนาดต่างกันคือรัศมีวงหนึ่งเป็นค่าลบจริงๆ


26
ตามที่ @Cubic กล่าวไว้คุณควรรายงานปัญหานี้ให้กับผู้ดูแลระบบ Gloss คำถามของคุณแสดงให้เห็นอย่างชัดเจนว่าอินสแตนซ์เด็กกำพร้าที่ไม่เหมาะสมของห้องสมุดทำให้โค้ดของคุณยุ่งเหยิงอย่างไร
Christian Conkle

1
เสร็จแล้ว เป็นไปได้ไหมที่จะยกเว้นอินสแตนซ์ พวกเขาอาจต้องการเพื่อให้ไลบรารีทำงานได้ แต่ฉันไม่ต้องการ ฉันยังสังเกตเห็นว่าพวกเขากำหนด Num Color เป็นเพียงเรื่องของเวลาก่อนที่จะมารบกวนฉัน
Carcigenicate

@Cubic อืมสายเกินไปแล้ว และฉันดาวน์โหลดมาเมื่อสัปดาห์ก่อนโดยใช้ Cabal ที่อัปเดตและเป็นปัจจุบัน ดังนั้นควรเป็นปัจจุบัน
Carcigenicate

2
@ChristianConkle มีโอกาสที่ผู้เขียนกลอสไม่เข้าใจว่า TypeSynonymInstances ทำอะไร ในกรณีใด ๆ นี้จริงๆต้องหายไป (ทั้งทำให้หรือใช้ชื่อผู้ประกอบการอื่น ๆ Ala )Pointnewtypelinear
Cubic

1
@Cubic: TypeSynonymIn
John L

คำตอบ:


128

วิธีเดียวที่อาจรวบรวมได้คือถ้ามีNum (Float,Float)อินสแตนซ์อยู่ สิ่งนี้ไม่ได้จัดเตรียมโดยไลบรารีมาตรฐานแม้ว่าอาจเป็นไปได้ว่าหนึ่งในไลบรารีที่คุณใช้เพิ่มเข้ามาด้วยเหตุผลบ้าๆ ลองโหลดโปรเจ็กต์ของคุณใน ghci และดูว่าใช้10 :: (Float,Float)งานได้หรือไม่จากนั้นลอง:i Numค้นหาว่าอินสแตนซ์มาจากไหนแล้วตะโกนว่าใครเป็นคนกำหนด

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


53
ว้าว. 10 :: (Float, Float)ให้ผลตอบแทน(10.0,10.0)และ:i Numมีบรรทัดinstance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’( Pointเป็นนามแฝงของพิกัดของ Gloss) อย่างจริงจัง? ขอบคุณ. นั่นช่วยฉันจากคืนที่นอนไม่หลับ
Carcigenicate

6
@Carcigenicate แม้ว่าจะดูไม่สำคัญที่จะอนุญาตให้ใช้อินสแตนซ์ดังกล่าว แต่เหตุผลที่อนุญาตคือเพื่อให้นักพัฒนาสามารถเขียนอินสแตนซ์ของตัวเองNumได้ว่ามันเหมาะสมตรงไหนเช่นAngleประเภทข้อมูลที่ จำกัดDoubleระหว่าง-piและpiหรือหากมีคนต้องการเขียนประเภทข้อมูล แทนควอเทอร์เนียนหรือตัวเลขอื่น ๆ ที่ซับซ้อนกว่าคุณสมบัตินี้สะดวกมาก นอกจากนี้ยังเป็นไปตามกฎเดียวกันกับString/ Text/ ByteStringทำให้อินสแตนซ์เหล่านี้สมเหตุสมผลจากมุมมองที่ใช้งานง่าย แต่สามารถใช้ในทางที่ผิดเช่นในกรณีนี้
bheklilr

4
@bheklilr ฉันเข้าใจถึงความจำเป็นในการอนุญาตอินสแตนซ์ของ Num "WOW" เกิดจากบางสิ่ง ฉันไม่รู้ว่าคุณสามารถสร้างอินสแตนซ์ประเภทนามแฝงได้การสร้างอินสแตนซ์ Num ของพิกัดดูเหมือนจะใช้งานง่ายและฉันก็ไม่ได้คิดถึงมัน ได้เรียนรู้บทเรียนแล้ว
Carcigenicate

3
คุณสามารถแก้ไขปัญหาของคุณกับอินสแตนซ์ orphaned จากไลบรารีของคุณได้โดยใช้การnewtypeประกาศCoordแทน a type.
Benjamin Hodgson

3
@Carcigenicate ฉันเชื่อว่าคุณต้องการ-XTypeSynonymInstancesเพื่ออนุญาตให้มีอินสแตนซ์สำหรับคำพ้องความหมายประเภท แต่ไม่จำเป็นต้องสร้างอินสแตนซ์ที่มีปัญหา อินสแตนซ์สำหรับNum (Float, Float)หรือแม้กระทั่ง(Floating a) => Num (a,a)ไม่จำเป็นต้องมีส่วนขยาย แต่จะส่งผลให้เกิดพฤติกรรมเดียวกัน
crockeea

64

ตัวตรวจสอบประเภทของ 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คำเหมือนถ้ามันสำคัญพอ คุณค่อนข้างมั่นใจได้ว่าจะไม่มีใครยุ่งกับพวกเขา

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

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