Предположим, что у нас есть следующий тип, описывающий вид дерева (дуб, сосна, берёза, …):

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