Перевірка типу 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 для перевірки.
Визначте власне newtype
s замість type
синонімів, якщо це досить важливо. Ви можете бути впевнені, що з ними ніхто не зіпсується.
Якщо у вас виникають часті проблеми, пов’язані з бібліотекою з відкритим кодом, ви, звичайно, можете скласти власну версію бібліотеки, але обслуговування може швидко стати головним болем.