Реализация Random для перечисляемого и ограниченного типа
Предположим, что у нас есть следующий тип, описывающий вид дерева (дуб, сосна, берёза, …):
data TreeType = Pine | Oak | Birch | Maple
Мы бы хотели уметь строить дерево случайного вида, т.е. использовать интерфейс тайпкласса Random
и комбинаторы из System.Random
:
Тайпкласс Random⌗
Тайпкласс случайных величин Random
в Haskell имеет следующий интерфейс:
randomR :: (RandomGen g, Random a) => (a, a) -> g -> (a, g)
random :: (RandomGen g, Random a) => g -> (a, g)
Обе функции принимают на вход детерминированный генератор псевдослучайных чисел, а функция randomR
также - мнимальное и максимальное
значение случайной величины, в интервале между которыми и будет сделан случайный выбор. Функции возвращают значение случайной величины
и новый генератор. Пример использования :
> g <- getStdGen -- текущий генератор из IO
> random g :: (Int, StdGen)
(-7421132080333353634,736230075 1591107922)
> random g :: (Float, StdGen)
(0.960122,1140094206 1309575449)
> random g :: (Bool, StdGen)
(False,745803261 289709180)
> randomR (10, 90) g :: (Int, StdGen)
(19,745803261 289709180)
> random g :: (Int, StdGen) -- генератор старый
(-7421132080333353634,736230075 1591107922)
Мы хотим делать то же самое с нашим типом TreeType
. Для этого нам надо сделать его экземпляром тайпкласса Random
, то есть реализовать
поверх TreeType
две вышеупомянутые функции (необходимое и достаточное условие тайпкласса Random
).
Тайпкласс Enum⌗
Тайпкласс Enum
представляет последовательно перечисляемые взад-вперёд типы данных. Членство в классе Enum
требует реализации двух функций:
toEnum :: Int -> a
fromEnum :: a -> Int
то есть инъекции из нашего типа в натуральные числа. Фактически, реализуя Enum TreeType
мы последовательно (с нуля) занумеровываем
конструкторы Pine
, Oak
и т.д.
Данные функции для заданного простым перечислением конструкторов типа могут быть построены компилятором автоматически:
Тайпкласс Bounded⌗
Тайпкласс Bounded
характеризует типы, имеющие некоторую “нижнюю” и “верхнюю” границы. Необходимая реализация:
minBound, maxBound :: a
Например, нижней границей Bool
является False
, а maxBound :: Int
составляет 9223372036854775807
(напротив, тип Integer
ограничен
только памятью компьютера).
Реализация Random TreeType⌗
Вернёмся к нашему типу:
data TreeType = Pine | Oak | Birch | Maple
deriving (Enum, Bounded)
Мы хотим написать randomR
и random
(посредством randomR
на всём интервале от minBound :: TreeType
до maxBound
). Проще простого: в
randomR
мы просто воспользуемся биекцией Enum TreeType
в подмножество натуральных чисел и выбирем конструктор типа TreeType
исходя из
случайного числа (то есть уже существующей реализации Random Int
). Сначала мы получаем натуральные числа-границы из нашего типа с помощью
fromEnum
, затем вызываем randomR
для чисел и полученное случайное число обратно преобразуем в конструктор TreeType
с toEnum
. Функция
random
для TreeType
же просто использует весь интервал нашего типа с minBound
и maxBound
. Немного обобщим:
abstractRandomR :: (Enum a, RandomGen g) => (a, a) -> g -> (a, g)
abstractRandomR (lo, hi) g = (v, g')
where v = toEnum n
(n, g') = randomR (fromEnum lo, fromEnum hi) g
abstractRandom :: (Enum a, Bounded a, RandomGen g) => g -> (a, g)
abstractRandom g = abstractRandomR (lo, hi) g
where lo = minBound
hi = maxBound
instance Random TreeType where
randomR = abstractRandomR
random = abstractRandom
Проверка⌗
Проверка в ghci:
> (minBound, maxBound) :: (TreeType, TreeType)
(Pine,Maple)
> g <- getStdGen
> random g :: (TreeType, StdGen)
(Birch,736230075 1591107922)
> randomR (Pine, Birch) g :: (TreeType, StdGen)
(Oak,736230075 1591107922)
>
> -- недетерминированный генератор
> getStdRandom random :: IO TreeType
Birch
> getStdRandom random :: IO TreeType
Pine
> getStdRandom random :: IO TreeType
Oak