Вы не можете создать чистую функцию с именем, 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 много раз для одной и той же случайной величины с разными семенами. Это, конечно, возможно в императивных языках, но в этом случае мы можем быть уверены, что не будем выполнять непредвиденные операции ввода-вывода каждый раз, когда выбираем случайную переменную, и нам не нужно быть осторожными при инициализации состояния.
bad :: Random a -> a
вводить несоответствия? Что в этом плохого? Пожалуйста, продвигайтесь медленно в объяснении, особенно для первых шагов :) Если бы вы могли объяснить, почему «полезные» функции полезны, это может быть ответ из 1000 пунктов! :)Вы не ошиблись. Если вы дадите одно и то же начальное число ГСЧ дважды, то первое возвращаемое им псевдослучайное число будет одинаковым. Это не имеет ничего общего с функциональным программированием против побочных эффектов; определение семени является то , что конкретный вход вызывает определенный вывод хорошо распределенных , но определенно не-случайных значений. Вот почему он называется псевдослучайным, и часто полезно иметь, например, писать предсказуемые модульные тесты, надежно сравнивать различные методы оптимизации для одной и той же проблемы и т. Д.
Если вы на самом деле хотите получить от компьютера не псевдослучайные числа, вам нужно подключить его к чему-то действительно случайному, например, к источнику распада частиц, непредсказуемым событиям, происходящим в сети, в которой работает компьютер, и т. Д. получить право и, как правило, дорого, даже если это работает, но это единственный способ не получить псевдослучайные значения (обычно значения, которые вы получаете от вашего языка программирования, основаны на некотором начальном числе, даже если вы явно не предоставили его).
Это и только это может поставить под угрозу функциональную природу системы. Поскольку генераторы, не являющиеся псевдослучайными, встречаются редко, это случается не часто, но да, если у вас действительно есть метод, генерирующий истинные случайные числа, то хотя бы этот маленький кусочек вашего языка программирования не может быть на 100% чисто функциональным. Будет ли язык делать исключение для него или нет, это просто вопрос прагматичности реализации языка.
источник
() -> Integer
. Вы можете иметь чисто функциональный тип PRNGPRNG_State -> (PRNG_State, Integer)
, но вам придется инициализировать его нечистыми средствами).Один из способов - думать о нем как о бесконечной последовательности случайных чисел:
То есть просто думайте об этом как о бездонной структуре данных, как о том,
Stack
куда вы можете только позвонитьPop
, но вы можете называть это вечно. Как и в обычном неизменном стеке, взятие одного из верха дает вам другой (другой) стек.Поэтому неизменный (с ленивым вычислением) генератор случайных чисел может выглядеть так:
Это функционально.
источник
pseudoRandom :: Seed -> (Seed, Integer)
. Вы могли бы даже написать функцию такого типа[Integer] -> ([Integer], Integer)
То же самое для не функциональных языков. Игнорирование слегка отдельной проблемы действительно случайных чисел здесь.
Генератор случайных чисел всегда принимает начальное значение и для одного и того же начального числа возвращает одну и ту же последовательность случайных чисел (очень полезно, если вам нужно протестировать программу, которая использует случайные числа). По сути, он начинается с выбранного вами начального числа, а затем использует последний результат в качестве начального значения для следующей итерации. Таким образом, большинство реализаций являются «чистыми» функциями в том виде, как вы их описываете: принимайте значение и для одного и того же значения всегда возвращайте один и тот же результат.
источник