Почему мы не можем иметь случайные экземпляры классов, производные для перечислений в Haskell?
Я написал это сегодня:
data Door = A | B | C
deriving (Eq,Bounded,Enum)
instance Random Door where
randomR (lo,hi) g = (toEnum i, g')
where (i,g') = randomR (fromEnum lo, fromEnum hi) g
random = randomR (minBound,maxBound)
И я понял, что это примерно копипастабильно для любого перечисления. Я попытался вставить слово "Рэндом" в выводящий пункт, но это не помогло.
Затем я поискал в интернете и нашел это:
Пожалуйста, предоставьте пример для (перечисление a, ограниченный a) для случайного #21
Несколько цитат, которые, кажется, отвечают на мой вопрос, но я их не совсем понимаю:
Какой экземпляр вы имеете в виду, instance (Bounded a, Enum a) => Случайный, где ...? Не может быть такого примера, так как это было бы перекрываются с каждым другим экземпляром.
Это предотвратит любые пользовательские производные экземпляры. ...
Почему это не может быть автоматизировано, либо с помощью производящего предложения, либо с помощью реализации по умолчанию, по крайней мере.
Почему это не сработает?
instance (Bounded a, Enum a) => Random a where
randomR (lo,hi) g = (toEnum i, g')
where (i,g') = randomR (fromEnum lo, fromEnum hi) g
random = randomR (minBound,maxBound)
1 ответ:
Комментарии ссылаются на то, что в Haskell (фактически в Haskell с расширением
FlexibleInstances
) сопоставление экземпляров выполняется путем сопоставления типа без учета ограничений. После того, как типовое соответствие успешно выполнено, ограничения затем проверяются и генерируют ошибки, если они не выполняются. Итак, если вы определяете:instance (Bounded a, Enum a) => Random a where ...
Вы фактически определяете экземпляр для каждого типа
a
, а не только для типовa
, которые имеют экземплярыBounded
иEnum
. Это как если бы ... вы написали:instance Random a where ...
Это будет потенциально конфликтовать с любыми другими экземплярами, определенными библиотекой или пользователем, такими как:
Есть способы обойти это, но в конечном итоге все становится довольно запутанным. Кроме того, это может привести к некоторым загадочным сообщениям об ошибках типа компиляции, как отмечено ниже.newtype Gaussian = Gaussian Double instance Random Gaussian where ...
В частности, если вы поместите в модуль следующее:
module RandomEnum where import System.Random instance (Bounded a, Enum a) => Random a where randomR (lo,hi) g = (toEnum i, g') where (i,g') = randomR (fromEnum lo, fromEnum hi) g random = randomR (minBound,maxBound)
Вы обнаружите, что вам нужно расширение
FlexibleInstances
, чтобы разрешить ограничения на экземпляры. Это прекрасно, но если вы добавите это, то увидите, что вам нужно расширениеUndecidableInstances
. Это, возможно, менее хорошо, но если вы добавите это, то обнаружите, что вы получите ошибку при вызовеrandomR
на RHS вашего определенияrandomR
. GHC определил, что экземпляр, который вы определили, теперь перекрывается со встроенным экземпляром дляInt
. (На самом деле это совпадение, чтоInt
является одновременноBounded
иEnum
- он также перекрывается со встроенным экземпляром дляDouble
, который является ни.)В любом случае, вы можете обойти это, сделав ваш экземпляр перекрывающимся, так что следующее:
{-# LANGUAGE FlexibleInstances, UndecidableInstances #-} module RandomEnum where import System.Random instance {-# OVERLAPPABLE #-} (Bounded a, Enum a) => Random a where randomR (lo,hi) g = (toEnum i, g') where (i,g') = randomR (fromEnum lo, fromEnum hi) g random = randomR (minBound,maxBound)
Будет фактически компилироваться.
Это в основном нормально, но вы можете закончить с некоторыми странными сообщениями об ошибках. Обычно используется следующая программа:
main = putStrLn =<< randomIO
Будет генерировать разумное сообщение об ошибке:
No instance for (Random String) arising from a use of `randomIO'
Но с приведенным выше примером он становится:
No instance for (Bounded [Char]) arising from a use of ‘randomIO’
Потому что ваш экземпляр совпадает с
Как бы то ни было, в целом сообщество Haskell избегало помещать подобные экземпляры catch-all в стандартную библиотеку. Тот факт, что они нуждаются в расширенииString
, но GHC не может найтиBounded String
ограничение.UndeciableInstances
и прагмахOVERLAPPABLE
и потенциально вводят кучу нежелательных экземпляров в программу, все это оставляет неприятный привкус во рту людей. Таким образом, хотя технически возможно добавить такой экземпляр кSystem.Random
, этого никогда не произойдет.Точно так же это было бы возможно разрешить
Random
автоматически выводиться для любых типов, которые являютсяEnum
иBounded
, но сообщество неохотно добавляет дополнительные механизмы автоматического вывода, особенно для классов типов, таких какRandom
, которые просто не так часто используются (по сравнению сShow
илиEq
). Так что, опять же, этого никогда не случится.Вместо этого стандартный способ разрешить удобные экземпляры по умолчанию-определить некоторые вспомогательные функции, которые могут использоваться в явном экземпляре определение, и это то, что предлагается в нижней части предложения, которое вы связали. Например, в
System.Random
можно определить следующие функции:defaultEnumRandomR :: (Enum a, RandomGen g) => (a, a) -> g -> (a, g) defaultEnumRandomR (lo,hi) g = (toEnum i, g') where (i,g') = randomR (fromEnum lo, fromEnum hi) g defaultBoundedRandom :: (Random a, Bounded a, RandomGen g) => g -> (a, g) defaultBoundedRandom = randomR (minBound, maxBound)
И люди напишут:
Это единственное решение, которое имеет шанс превратить его вinstance Random Door where randomR = defaultEnumRandomR random = defaultBoundedRandom
System.Random
.Если это так, и вам не нравится определять явные экземпляры, вы можете придерживаться:
instance {-# OVERLAPPABLE #-} (Bounded a, Enum a) => Random a where randomR = defaultEnumRandomR random = defaultBoundedRandom
В вашем собственном коде.