Вы не можете создать чистую функцию с именем, random
которая будет давать разные результаты при каждом вызове. На самом деле, вы даже не можете «вызывать» чистые функции. Вы применяете их. Так что вы ничего не упускаете, но это не означает, что случайные числа в функциональном программировании запрещены. Позвольте мне продемонстрировать, что я буду использовать синтаксис Haskell повсюду.
Исходя из императивного фона, вы можете изначально ожидать, что случайный тип будет иметь следующий вид:
random :: () -> Integer
Но это уже исключено, потому что случайный не может быть чистой функцией.
Рассмотрим идею стоимости. Ценность является неизменной вещью. Он никогда не меняется, и каждое наблюдение, которое вы можете сделать по этому поводу, является постоянным на все времена.
Понятно, что random не может дать целочисленное значение. Вместо этого он генерирует случайную переменную типа Integer. Это тип может выглядеть так:
random :: () -> Random Integer
За исключением того, что передача аргумента совершенно не нужна, функции чисты, поэтому одна из random ()
них так же хороша, как и другая random ()
. Я дам случайный, с этого момента, этот тип:
random :: Random Integer
Что все хорошо, но не очень полезно. Вы можете ожидать, что сможете писать выражения как random + 42
, но не можете, потому что это не проверяет тип. Вы ничего не можете сделать со случайными переменными, пока.
Это поднимает интересный вопрос. Какие функции должны существовать для работы со случайными переменными?
Эта функция не может существовать:
bad :: Random a -> a
любым полезным способом, потому что тогда вы могли бы написать:
badRandom :: Integer
badRandom = bad random
Что вносит несоответствие. badRandom, предположительно, является значением, но это также случайное число; противоречие.
Может быть, мы должны добавить эту функцию:
randomAdd :: Integer -> Random Integer -> Random Integer
Но это всего лишь частный случай более общего паттерна. Вы должны иметь возможность применять любую функцию к случайным вещам, чтобы получить другие случайные вещи, например:
randomMap :: (a -> b) -> Random a -> Random b
Вместо того чтобы писать random + 42
, теперь мы можем писать randomMap (+42) random
.
Если бы все, что у вас было, было randomMap, вы бы не смогли объединить случайные переменные вместе. Вы не могли бы написать эту функцию, например:
randomCombine :: Random a -> Random b -> Random (a, b)
Вы можете попробовать написать это так:
randomCombine a b = randomMap (\a' -> randomMap (\b' -> (a', b')) b) a
Но это неправильный тип. Вместо того, чтобы в конечном итоге с Random (a, b)
, мы в конечном итоге сRandom (Random (a, b))
Это можно исправить, добавив еще одну функцию:
randomJoin :: Random (Random a) -> Random a
Но по причинам, которые могут в конечном итоге выясниться, я не собираюсь этого делать. Вместо этого я собираюсь добавить это:
randomBind :: Random a -> (a -> Random b) -> Random b
Не сразу очевидно, что это на самом деле решает проблему, но это так:
randomCombine a b = randomBind a (\a' -> randomMap (\b' -> (a', b')) b)
Фактически, можно написать randomBind в терминах randomJoin и randomMap. Также возможно написать randomJoin в терминах randomBind. Но я оставлю это как упражнение.
Мы могли бы немного упростить это. Позвольте мне определить эту функцию:
randomUnit :: a -> Random a
randomUnit превращает значение в случайную величину. Это означает, что у нас могут быть случайные переменные, которые на самом деле не случайны. Это всегда было так, хотя; мы могли бы сделать randomMap (const 4) random
раньше. Хорошей идеей для определения randomUnit является то, что теперь мы можем определить randomMap в терминах randomUnit и randomBind:
randomMap :: (a -> b) -> Random a -> Random b
randomMap f x = randomBind x (randomUnit . f)
Хорошо, теперь мы куда-то добираемся. У нас есть случайные величины, которыми мы можем манипулировать. Тем не мение:
- Не очевидно, как мы могли бы на самом деле реализовать эти функции,
- Это довольно громоздко.
Реализация
Я буду решать псевдослучайные числа. Возможно реализовать эти функции для реальных случайных чисел, но этот ответ уже становится довольно длинным.
По сути, способ, которым это сработает, заключается в том, что мы собираемся передать начальную ценность повсюду. Всякий раз, когда мы генерируем новое случайное значение, мы создаем новое начальное число. В конце, когда мы закончим создание случайной переменной, мы захотим взять из нее выборку, используя эту функцию:
runRandom :: Seed -> Random a -> a
Я собираюсь определить тип Random следующим образом:
data Random a = Random (Seed -> (Seed, a))
Затем нам просто нужно предоставить реализации randomUnit, randomBind, runRandom и random, что довольно просто:
randomUnit :: a -> Random a
randomUnit x = Random (\seed -> (seed, x))
randomBind :: Random a -> (a -> Random b) -> Random b
randomBind (Random f) g =
Random (\seed ->
let (seed', x) = f seed
Random g' = g x in
g' seed')
runRandom :: Seed -> Random a -> a
runRandom seed (Random f) = (snd . f) seed
Для случайного, я собираюсь предположить, что уже есть функция типа:
psuedoRandom :: Seed -> (Seed, Integer)
В этом случае случайный просто Random psuedoRandom
.
Делать вещи менее громоздкими
У Haskell есть синтаксический сахар, чтобы сделать такие вещи приятнее на глазах. Это называется do-notation, и чтобы использовать все это, нам нужно создать экземпляр Monad for Random.
instance Monad Random where
return = randomUnit
(>>=) = randomBind
Готово. randomCombine
из раньше можно было теперь написать:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = do
a' <- a
b' <- b
return (a', b')
Если бы я делал это для себя, я бы даже пошел на шаг дальше и создал бы экземпляр Applicative. (Не волнуйтесь, если это не имеет смысла).
instance Functor Random where
fmap = liftM
instance Applicative Random where
pure = return
(<*>) = ap
Тогда randomCombine может быть написано:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = (,) <$> a <*> b
Теперь, когда у нас есть эти экземпляры, мы можем использовать >>=
вместо randomBind, join вместо randomJoin, fmap вместо randomMap, return вместо randomUnit. Мы также получаем целый набор функций бесплатно.
Стоит ли оно того? Можно утверждать, что добраться до этой стадии, где работа со случайными числами не является полностью ужасной, было довольно сложно и многословно. Что мы получили в обмен на эти усилия?
Самое непосредственное вознаграждение заключается в том, что теперь мы можем точно видеть, какие части нашей программы зависят от случайности, а какие являются полностью детерминированными. По моему опыту, принуждение к такому строгому разделению чрезвычайно упрощает ситуацию.
Мы до сих пор предполагали, что нам просто нужна отдельная выборка из каждой случайной величины, которую мы генерируем, но если окажется, что в будущем мы действительно хотим увидеть больше распределения, это тривиально. Вы можете просто использовать runRandom много раз для одной и той же случайной величины с разными семенами. Это, конечно, возможно в императивных языках, но в этом случае мы можем быть уверены, что не будем выполнять непредвиденные операции ввода-вывода каждый раз, когда выбираем случайную переменную, и нам не нужно быть осторожными при инициализации состояния.