Проверка типов в 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, где есть настоящие глобальные эффекты; вам нужно будет проверить, что каждый модуль, который вы импортируете, и каждый модуль, который они импортируют, не реализуют бесхозные экземпляры. Аннотации типов могут иногда предупреждать вас о проблемах, и, конечно, вы можете использовать :iGHCi для проверки.
Определите свои собственные newtypes вместо typeсинонимов, если это достаточно важно. Вы можете быть уверены, что никто не станет с ними связываться.
Если у вас часто возникают проблемы, связанные с библиотекой с открытым исходным кодом, вы, конечно, можете создать свою собственную версию библиотеки, но обслуживание может быстро стать головной болью.