(Отредактировано) Как получить случайное число в Haskell без ввода-вывода

Я хочу иметь функцию, которая возвращает разные stdGen в каждом вызове без ввода-вывода. Я пытался использовать unsafePerformIO в качестве следующего кода.

import System.IO.Unsafe
import System.Random

myStdGen :: StdGen
myStdGen = unsafePerformIO getStdGen

Но когда я пытаюсь вызвать myStdGen в ghci, я всегда получаю одно и то же значение. Я злоупотреблял unsafePerformIO? Или есть другие способы достичь моей цели?

ИЗМЕНИТЬ Извините, я думаю, что должен более точно описать свой вопрос.

На самом деле, я реализую вариант структуры данных treap, для которого требуется специальная операция «слияния». Он полагается на некоторую случайность, чтобы гарантировать амортизированную O (log n) ожидаемую временную сложность.

Я пытался использовать такую ​​пару, как (Tree, StdGen), чтобы сохранить генератор случайных чисел для каждого набора. При вставке новых данных в treap я бы использовал random, чтобы присвоить случайное значение новому узлу, а затем обновить свой генератор. Но я столкнулся с проблемой. У меня есть функция с именем empty, которая возвращает пустой набор, и я использовал функцию myStdGen выше, чтобы получить генератор случайных чисел для этого набора. Однако, если у меня есть два пустых трепа, их StdGen будет одинаковым. Поэтому после того, как я вставил данные как в treap, так и когда я хочу их объединить, их случайное значение тоже будет одинаковым. Поэтому я потерял случайность, на которую полагаюсь.

Вот почему я хотел бы иметь какой-то «глобальный» генератор случайных чисел, который выдает разные StdGen для каждого вызова, чтобы каждый пустой треп мог иметь разные StdGen.


person arbuztw    schedule 20.11.2014    source источник
comment
Да, это злоупотребление. Если что-то возвращает разные результаты с одинаковыми аргументами, это не (математическая) функция. Вы не можете иметь это. Вы можете сделать это в какой-нибудь монаде, это не обязательно должен быть IO, но он должен сохранять какое-то состояние.   -  person augustss    schedule 20.11.2014


Ответы (3)


Это не очень хорошее использование unsafePerformIO.

Причина, по которой вы постоянно видите одно и то же число в GHCi, заключается в том, что сам GHCi не знает, что значение нечисто, и поэтому помнит значение с момента последнего вызова. Вы можете вводить команды ввода-вывода на верхнем уровне GHCi, поэтому вы ожидаете увидеть другое значение, если просто наберете getStdGen. Однако это тоже не сработает из-за неясной части работы GHCi, включающей невозврат выражений верхнего уровня. Вы можете включить это с помощью :set +r:

> :set +r
> getStdGen
2144783736 1
> getStdGen
1026741422 1

Обратите внимание, что ваша нечистая функция, притворяющаяся чистой, все равно не будет работать.

> myStdGen
480142475 1
> myStdGen
480142475 1
> myStdGen
480142475 1

Вы действительно не хотите идти по этому пути. unsafePerformIO не должен использоваться таким образом, да и вообще это не очень хорошая идея. Есть способы получить то, что вы хотели (например, unsafePerformIO randomIO :: Int), но они не приведут вас к чему-то хорошему. Вместо этого вы должны выполнять вычисления на основе случайных чисел внутри случайной монады и запускать их в монаде IO.

Обновлять

Я вижу из вашего обновления, почему вы хотели этого в первую очередь.

В этом ответе есть много интересных мыслей о проблеме случайности внутри ссылочно прозрачных функций. .

Несмотря на то, что некоторые люди выступают за использование unsafePerformIO в этом случае, это все еще плохая идея по ряду причин, которые изложены в различных частях этой страницы. В конце концов, если функция зависит от источника случайности, лучше всего указать это в ее типе, и самый простой способ сделать это — поместить ее в случайную монаду. Это по-прежнему чистая функция, просто она принимает генератор при вызове. Вы можете предоставить этот генератор, запросив случайный генератор в основной подпрограмме IO.

Хороший пример использования случайной монады можно найти здесь.

person Vic Smith    schedule 20.11.2014
comment
Интересное о :set +r. Однако в моем ghci-7.8.2 повторение getStdGen всегда дает одно и то же... - person leftaroundabout; 20.11.2014
comment
Я все еще задерживаюсь в 7.6.3. Странно, что это поведение, похоже, изменилось. - person Vic Smith; 20.11.2014

Я злоупотреблял unsafePerformIO

Черт, да! «Отличительные черты чистой функции»

  • Нет побочных эффектов
  • Ссылочная прозрачность, т. е. каждая последующая оценка результата должна давать один и тот же результат.

На самом деле способ достижения вашей «цели» есть, но идея просто неверна.

myStdGen :: () -> StdGen
myStdGen () = unsafePerformIO getStdGen

Поскольку это (бесполезный) вызов функции вместо CAF, он оценит действие IO при каждом звонке отдельно.

Тем не менее, я думаю, что компилятор в значительной степени может оптимизировать это, поэтому определенно не полагайтесь на него.

ИЗМЕНИТЬ при попытке я заметил, что сам getStdGen всегда дает один и тот же генератор при использовании в данном процессе, поэтому, даже если вышеприведенное будет использовать более разумные типы, это не сработает.


Обратите внимание, что правильное использование псевдослучайности в алгоритмах и т. д. не требует ввода-вывода везде, например, вы можете вручную заполнить StdGen, но затем правильно распространить его с помощью split и т. д. Гораздо более приятный способ обработки то есть с монадой случайности< /а>. Тогда программа в целом всегда будет давать один и тот же результат, но внутри будет иметь все разные случайные числа, необходимые для полезной работы.

В качестве альтернативы вы можете получить генератор от IO, но все же написать свой алгоритм в чистой случайной монаде, а не в IO.


Есть еще один способ получить «случайность» в полностью чистом алгоритме: потребовать, чтобы ввод был Hashable! Затем вы можете эффективно использовать любой аргумент функции в качестве случайного начального числа. Это немного странное решение, но оно может сработать для вашего приложения treap (хотя я думаю, что некоторые люди больше не будут классифицировать его как treap, а как особый вид хэш-карты).

person leftaroundabout    schedule 20.11.2014
comment
Я думаю, вы пропустили NOINLINE. Я также думаю, что на самом деле это разумно хотеть, но я не думаю, что можно гарантировать, что это будет работать правильно — хотя результаты одинаковы до (==), я думаю, что оптимизатор может быть в состоянии сильно сломать что-то — если он решит пересчитать что-то, а не сохранить это, черт возьми, все может сорваться. - person dfeuer; 20.11.2014
comment
Спасибо за ваше предложение. Но я не уверен, что это сработает. Потому что целью случайного ключа в treap является предотвращение наихудшего случая для BST (цепочки), которая зависит от содержащихся в ней ключей. Но единственным входом функции является ключ, поэтому случайное значение также будет зависеть от вставленных ключей. Однако в общих случаях я думаю, что худший случай может и не случиться. :) - person arbuztw; 20.11.2014

Да, вы оскорбили unsafePerformIO. Есть очень мало веских причин для использования unsafePerformIO, например, при взаимодействии с библиотекой C, и он также используется в реализации нескольких основных библиотек (я думаю, что ST является одной из них). Короче говоря, не используйте unsafePerformIO, если вы действительно не уверены в том, что делаете. Он не предназначен для генерации случайных чисел.

Напомним, что функции в Haskell должны быть чистыми, а это означает, что они зависят только от своих входных данных. Это означает, что у вас может быть чистая функция, которая генерирует «случайное» число, но это число зависит от генератора случайных чисел, который вы ему передаете, вы можете сделать что-то вроде

myStdGen :: StdGen
myStdGen = mkStdGen 42

Тогда вы могли бы сделать

randomInt :: StdGen -> (Int, StdGen)
randomInt g = random

Но тогда вы должны использовать новый StdGen, возвращаемый этой функцией, в дальнейшем, иначе вы всегда будете получать один и тот же результат от randomInt.


Итак, вам может быть интересно, как правильно генерировать случайные числа, не прибегая к IO? Ответ — монада State. Это похоже на

newtype State s a = State { runState :: s -> (a, s) }

И его экземпляр монады выглядит так

instance Monad (State s) where
    return a = State $ \s -> (a, s)
    (State m) >>= f = State $ \s -> let (a, newState) = m s
                                        (State g)     = f a
                                    in g newState

На первый взгляд это немного сбивает с толку, но, по сути, все, что делает монада состояния, — это причудливая композиция функций. Подробнее см. LYAH. Здесь важно отметить, что тип s не меняется между шагами, может измениться только параметр a.

Вы заметите, что s -> (a, s) очень похоже на нашу функцию StdGen -> (Int, StdGen) с s ~ StdGen и a ~ Int. Это означает, что если бы мы сделали

randomIntS :: State StdGen Int
randomIntS = State randomInt

Тогда мы могли бы сделать

twoRandInts :: State StdGen (Int, Int)
twoRandInts = do
    a <- randomIntS
    b <- randomIntS
    return (a, b)

Затем его можно запустить, указав начальное состояние:

main = do
    g <- getStdGen
    print $ runState twoRandInts g

StdGen по-прежнему выходит из IO, но тогда вся логика происходит исключительно внутри монады состояния.

person bheklilr    schedule 20.11.2014