Почему мы не можем иметь случайные экземпляры классов, производные для перечислений в 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 2

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’

Потому что ваш экземпляр совпадает с String , но GHC не может найти Bounded String ограничение.

Как бы то ни было, в целом сообщество Haskell избегало помещать подобные экземпляры catch-all в стандартную библиотеку. Тот факт, что они нуждаются в расширении 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

В вашем собственном коде.