Пытаясь отладить проблему в моей программе (2 круга с одинаковым радиусом рисуются для разных размеров с помощью Gloss *
), я наткнулся на странную ситуацию. В моем файле, который обрабатывает объекты, у меня есть следующее определение для 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
in startPlayer
вызывает соответствующий ответ (изменяет начальный размер Player
); опять же, несмотря на то, что он не того типа. Чтобы убедиться, что определение данных правильно читается, я вставил в файл опечатку, и это дало мне ошибку; поэтому я ищу правильный файл.
Я попытался вставить 2 приведенных выше фрагмента в их собственный файл, и он выплюнул ожидаемую ошибку, что второе поле Player
in startPlayer
неверно.
Что могло позволить этому случиться? Можно подумать, что это как раз то, что должна предотвратить проверка типов в Haskell.
*
Ответ на мою первоначальную проблему, когда два круга предположительно равного радиуса были нарисованы разного размера, заключался в том, что один из радиусов на самом деле был отрицательным.
Point
a,newtype
либо используйте другие имена операторов alalinear
)Ответы:
Единственный способ, которым это может быть скомпилировано, - это если существует
Num (Float,Float)
экземпляр. Это не предусмотрено стандартной библиотекой, хотя возможно, что одна из используемых вами библиотек добавила его по какой-то безумной причине. Попробуйте загрузить свой проект в ghci и посмотреть,10 :: (Float,Float)
работает ли он , затем попытайтесь:i Num
выяснить, откуда исходит экземпляр, а затем кричите на того, кто его определил.Приложение: Нет возможности отключить инстансы. Нет даже способа не экспортировать их из модуля. Если бы это было возможно, это привело бы к еще более запутанному коду. Единственное реальное решение здесь - не определять такие экземпляры.
источник
10 :: (Float, Float)
дает(10.0,10.0)
и:i Num
содержит строкуinstance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’
(Point
это псевдоним Coord для Gloss). Шутки в сторону? Спасибо. Это спасло меня от бессонной ночи.Num
там, где это имеет смысл, например,Angle
тип данных, который ограничиваетDouble
между-pi
иpi
, или если кто-то хочет написать тип данных Представляя кватернионы или какой-либо другой более сложный числовой тип, эта функция очень удобна. Он также следует тем же правилам, что иString
/Text
/ByteString
, разрешая эти экземпляры, имеет смысл с точки зрения простоты использования, но его можно использовать неправильно, как в этом случае.newtype
объявление forCoord
вместо файлаtype
.Num (Float, Float)
или даже(Floating a) => Num (a,a)
не потребует расширения, но приведет к тому же поведению.Проверка типов в 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
«sPoint
буквально эквивалентны.Поэтому, когда
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 Kmettlinear
(для типов* -> *
). Это то, чем я обычно занимаюсь.Используйте
-Wall
. Не реализуйте сиротские экземпляры и не отключайте предупреждение о сиротских экземплярах.В качестве альтернативы, следуйте примеру
linear
и многим другим хорошо управляемым библиотекам и предоставьте бесхозные экземпляры в отдельном модуле, оканчивающемся на.OrphanInstances
или.Instances
. И не импортируйте этот модуль из любого другого модуля . Затем пользователи могут явно импортировать сирот, если захотят.Если вы обнаружите, что определяете сирот, подумайте о том, чтобы попросить разработчиков апстрима реализовать их вместо этого, если это возможно и целесообразно. Я часто писал сиротский экземпляр
Show a => Show (Identity a)
, пока его не добавили вtransformers
. Возможно, я даже написал об этом отчет об ошибке; Не помню.Что делать - пользователям библиотеки
У вас не так много вариантов. Обратитесь - вежливо и конструктивно! - к сопровождающим библиотеки. Укажите им на этот вопрос. У них могла быть какая-то особая причина написать проблемному сироте, или они могут просто не осознавать.
В более широком смысле: помните об этой возможности. Это одна из немногих областей Haskell, где есть настоящие глобальные эффекты; вам нужно будет проверить, что каждый модуль, который вы импортируете, и каждый модуль, который они импортируют, не реализуют бесхозные экземпляры. Аннотации типов могут иногда предупреждать вас о проблемах, и, конечно, вы можете использовать
:i
GHCi для проверки.Определите свои собственные
newtype
s вместоtype
синонимов, если это достаточно важно. Вы можете быть уверены, что никто не станет с ними связываться.Если у вас часто возникают проблемы, связанные с библиотекой с открытым исходным кодом, вы, конечно, можете создать свою собственную версию библиотеки, но обслуживание может быстро стать головной болью.
источник