Проверка типов в Haskell разумна. Проблема в том, что авторы библиотеки, которую вы используете, сделали что-то ... менее разумное.
Краткий ответ: да, 10 :: (Float, Float)
совершенно верно, если есть экземпляр Num (Float, Float)
. В этом нет ничего «очень плохого» с точки зрения компилятора или языка. Это просто не согласуется с нашей интуицией о том, что делают числовые литералы. Поскольку вы привыкли к тому, что система типов улавливает подобную ошибку, вы вполне удивлены и разочарованы!
Num
примеры и fromInteger
проблема
Вы удивлены , что компилятор принимает 10 :: Coord
, то есть 10 :: (Float, Float)
. Разумно предположить, что числовые литералы, например, 10
будут иметь «числовые» типы. Из коробки, числовые литералы можно интерпретировать как Int
, Integer
, Float
, или Double
. Набор чисел без какого-либо другого контекста не похож на число в том смысле, в котором эти четыре типа являются числами. Мы не о чем Complex
.
Однако, к счастью или к сожалению, Haskell - очень гибкий язык. Стандарт определяет, что целочисленный литерал like 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"
это не делает его лучше. Будут ли ваши пользователи сбиты с толку или удивлены - или, что еще хуже, никогда не заметят, - если их целочисленные литералы будут случайно выведены как имеющие тип, который вы создаете? Является ли обеспечение (*)
и (+)
это критичным - особенно если вам нужно его взломать?
Рассмотрите возможность использования альтернативных арифметических операторов, определенных в библиотеке, такой как Conal Elliott vector-space
(для типов *
) или Edward Kmett linear
(для типов * -> *
). Это то, чем я обычно занимаюсь.
Используйте -Wall
. Не реализуйте сиротские экземпляры и не отключайте предупреждение о сиротских экземплярах.
В качестве альтернативы, следуйте примеру linear
и многим другим хорошо управляемым библиотекам и предоставьте бесхозные экземпляры в отдельном модуле, оканчивающемся на .OrphanInstances
или .Instances
. И не импортируйте этот модуль из любого другого модуля . Затем пользователи могут явно импортировать сирот, если захотят.
Если вы обнаружите, что определяете сирот, подумайте о том, чтобы попросить разработчиков апстрима реализовать их вместо этого, если это возможно и целесообразно. Я часто писал сиротский экземпляр Show a => Show (Identity a)
, пока его не добавили в transformers
. Возможно, я даже написал об этом отчет об ошибке; Не помню.
Что делать - пользователям библиотеки
У вас не так много вариантов. Обратитесь - вежливо и конструктивно! - к сопровождающим библиотеки. Укажите им на этот вопрос. У них могла быть какая-то особая причина написать проблемному сироте, или они могут просто не осознавать.
В более широком смысле: помните об этой возможности. Это одна из немногих областей Haskell, где есть настоящие глобальные эффекты; вам нужно будет проверить, что каждый модуль, который вы импортируете, и каждый модуль, который они импортируют, не реализуют бесхозные экземпляры. Аннотации типов могут иногда предупреждать вас о проблемах, и, конечно, вы можете использовать :i
GHCi для проверки.
Определите свои собственные newtype
s вместо type
синонимов, если это достаточно важно. Вы можете быть уверены, что никто не станет с ними связываться.
Если у вас часто возникают проблемы, связанные с библиотекой с открытым исходным кодом, вы, конечно, можете создать свою собственную версию библиотеки, но обслуживание может быстро стать головной болью.