Перевірка типу 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пакет.
Введіть синоніми та осиротілі екземпляри
Але потримайте хвилину. Ви не використовуєте жодного glossтипу в цьому прикладі; чому примірник glossвплинув на вас? Відповідь надходить у два кроки.
По-перше, синонім типу, введений із ключовим словом type, не створює нового типу . У вашому модулі написання Coord- це просто стенограма (Float, Float). Так само в Graphics.Gloss.Data.Point, Pointозначає (Float, Float). Іншими словами, ваші Coordі gloss's 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, а кортежі та Floatвизначено в Prelude, тому все про те, як працюють ці три речі, визначено в Prelude. Чому імпорт зовсім іншого модуля може щось змінити? В ідеалі це не було б, але сиротні випадки порушують цю інтуїцію.
(Зверніть увагу, що GHC попереджає про випадки glossосиротіння. Автори спеціально перекреслили це попередження. Це повинно було підняти червоний прапор і запропонувати хоча б попередження в документації.)
Екземпляри класу є глобальними і їх не можна приховати
Крім того, екземпляри класу є глобальними : будь-який екземпляр, визначений у будь-якому модулі, який транзитивно імпортується з вашого модуля, буде в контексті та доступний для перевірки типу під час вирішення екземпляра. Це робить зручне глобальне міркування, оскільки ми можемо (як правило) припускати, що функція класу, як (+)і завжди, буде однаковою для даного типу. Однак це також означає, що місцеві рішення мають глобальний вплив; Визначення екземпляра класу безповоротно змінює контекст нижнього коду, не маючи можливості замаскувати або приховати його за межами модуля.
Ви не можете використовувати списки імпорту, щоб уникнути імпортування екземплярів . Так само ви не можете уникнути експорту екземплярів із визначених вами модулів.
Це проблемна і багато обговорювана область дизайну мови Haskell. У цій темі reddit є захоплююча дискусія пов'язаних питань . Дивіться, наприклад, коментар Едварда Кметта щодо дозволу контролю видимості для примірників: "Ви в основному викидаєте правильність майже всього коду, який я написав".
(До речі, як показала ця відповідь , ви можете порушити припущення про глобальний примірник в деяких аспектах, використовуючи осиротілі екземпляри!)
Що робити - для виконавців бібліотеки
Подумайте двічі перед впровадженням Num. Поки Ви не можете обійти цю fromIntegerпроблему, не, визначаючи fromInteger = error "not implemented"це НЕ робить його краще. Чи будуть ваші користувачі розгублені чи здивовані, або, що ще гірше, ніколи не помітять, якщо їх цілі буквені знаки випадково роблять висновок щодо типу, який ви створюєте? Чи надає це, (*)і (+)що критично важливо - особливо якщо вам доведеться його зламати?
Подумайте про використання альтернативних арифметичних операторів, визначених у бібліотеці, таких як Конал Елліотт vector-space(для типів роду *) або Едвард Кметт linear(для типів роду * -> *). Це те, що я схильний робити сам.
Використовуйте -Wall. Не реалізовуйте осиротілі екземпляри та не вимикайте попередження про осиротілий екземпляр.
Крім того, слідкуйте за керівництвом linearта багатьма іншими чудотворними бібліотеками та надайте сиротні екземпляри в окремий модуль, що закінчується на .OrphanInstancesабо .Instances. І не імпортуйте цей модуль з будь-якого іншого модуля . Тоді користувачі можуть імпортувати дітей-сиріт явно, якщо хочуть.
Якщо ви виявите, що ви визначаєте сиріт, подумайте, чи потрібно, якщо це можливо і доцільно, попросити технічних працівників, які їх надають. Я часто писав екземпляр-сироту Show a => Show (Identity a), поки вони не додали його transformers. Я, можливо, навіть підняв звіт про помилку; Я не пам'ятаю.
Що робити - для споживачів бібліотеки
У вас не багато варіантів. Вийдіть - ввічливо та конструктивно! - до бібліотечних працівників. Вкажіть їх на це питання. У них, можливо, були якісь особливі причини писати проблемну сироту, або вони просто не усвідомлюють.
Більш широко: пам’ятайте про таку можливість. Це одна з небагатьох областей Хаскелл, де є справжні глобальні ефекти; вам доведеться перевірити, чи кожен модуль, який ви імпортуєте, і кожен модуль, який вони імпортують, не реалізують осиротілі екземпляри. Анотації про тип іноді можуть попереджати про проблеми, і звичайно ви можете скористатися :iв GHCi для перевірки.
Визначте власне newtypes замість typeсинонімів, якщо це досить важливо. Ви можете бути впевнені, що з ними ніхто не зіпсується.
Якщо у вас виникають часті проблеми, пов’язані з бібліотекою з відкритим кодом, ви, звичайно, можете скласти власну версію бібліотеки, але обслуговування може швидко стати головним болем.