Как связать бинарные функции в Haskell?

Я хочу сгенерировать N чисел, используя функцию System.Random.next. Я реализовал функцию, которая принимает StdGen и список чисел и возвращает новый генератор и обновленный список чисел:

import System.Random  

getRandNum :: StdGen -> [Int] -> (StdGen, [Int])
getRandNum gen nums = (newGen, newNums) 
                where
                (randNum, newGen) = next gen
                newNums = nums ++ [randNum]

Затем я могу использовать его следующим образом:

λ: getRandNum (mkStdGen 1) []
(80028 40692,[39336])

Но у меня проблема с выполнением этой функции N раз, чтобы получить список случайных чисел. Как я могу правильно связать это?

Я также попробовал рекурсивный способ — он работает, но я уверен, что это решение далеко не элегантное:

randomnumbers_ :: Int -> StdGen -> (StdGen, [Int])
randomnumbers_ 0 gen = (gen, [])
randomnumbers_ 1 gen = (newGen, [randNum])
  where
    (randNum, newGen) = next gen
randomnumbers_ n gen = (newGen2, nums ++ nums2)
  where
    (newGen, nums) = randomnumbers_ 1 gen
    (newGen2, nums2) = randomnumbers_ (n - 1) newGen

randomnumbers :: Int -> Int -> [Int]
randomnumbers n seed = snd $ randomnumbers_ n generator
  where
    generator = mkStdGen seed

Кстати да, я знаю что это можно реализовать с помощью монады State и знаю как это сделать.


person trivelt    schedule 19.03.2021    source источник
comment
чтобы ответить на заголовок вашего вопроса, мы можем составить двоичные функции, которые возвращают кортежи одинаковой природы, используя curry и uncurry по мере необходимости. как вы упомянули, это то, что делает State Monad. или вручную распаковав кортежи и используя соответствующие значения в цепных или рекурсивных вызовах функций, как показано в ответах.   -  person Will Ness    schedule 19.03.2021


Ответы (3)


Способ связать его состоит в том, чтобы реализовать его таким образом, чтобы вам не нужно было это делать, то есть чтобы он выполнял цепочку сам по себе.

В любом случае в вашем коде есть неотъемлемое противоречие, вы называете его getRandNum, единственное число, и он действительно получает только одно число, но тип [Int].

Итак, мы решаем все это с минимальными изменениями в вашем коде, как

getRandNums :: StdGen -> [Int]
getRandNums gen = randNum : newNums 
                where
                (randNum, newGen) = next gen
                newNums = getRandNums newGen 

Такая схема типична для Haskell: списки строятся сверху вниз с использованием так называемой защищенной рекурсии, т. е. с рекурсией, защищенной ленивым конструктором данных (в данном случае :). Списки, построенные с повторным добавлением одиночек, как вы, очень неэффективны, имеют квадратичное поведение при доступе.

Наличие newGen надежно спрятанного внутри, инкапсулированного, является дополнительным бонусом. Вы же не хотите, чтобы это выставлялось напоказ, какой в ​​этом смысл? Перезапуск последовательности генерации случайных чисел с середины в любом случае просто воссоздает ту же последовательность чисел.

И, конечно же, вы можете убрать из этого списка столько номеров, сколько пожелаете, с помощью take n.

person Will Ness    schedule 19.03.2021

Ваше рекурсивное решение в порядке, и мы можем сократить его, удалив 1 случай, вставив то, где вы вызываете randomnumbers_ 1 gen.

randomnumbers_ :: Int -> StdGen -> (StdGen, [Int])
randomnumbers_ 0 gen = (gen, [])
randomnumbers_ n gen = (newGen2, num : nums)
   where
   (num, newGen) = next gen
   (newGen2, nums) = randomnumbers_ (n -1) newGen

Мы могли бы улучшить это, поменяв местами пары: странно, что стандартный next возвращает генератор во вторую позицию, а randomnumbers_ возвращает генератор в первую позицию.

Можно также устранить необходимость переноса состояния генератора, используя монаду состояния или какую-либо другую монаду, связанную со случайным образом, из библиотек (я не уверен в том, что они недавно добавили). Если вы работаете с большим количеством функций, генерирующих случайные числа, и носить с собой gen становится обременительно, такая монада, вероятно, является правильным инструментом.

person chi    schedule 19.03.2021

Немонадный стиль:

Предполагая, что вам может потребоваться конечное состояние генератора для дальнейшей обработки, вы можете написать свою функцию следующим образом, используя цепочку состояний генератора вручную:

import  System.Random

randomNumbers :: Int -> StdGen -> (StdGen, [Int])
randomNumbers count gen0 =
    if (count <= 0)
        then  (gen0, [])
        else  let  (v0, gen1) = next gen0
                   (gen2, vs) = randomNumbers (count-1) gen1
              in
                   (gen2, v0:vs)

Возможные улучшения:

  1. форсировать заданный выходной диапазон, а не брать родной генератор
  2. разрешить любой тип генератора, а не только StdGen

Это дало бы этот аналогичный код:

randomNumbersInRange :: RandomGen gt => (Int, Int) -> Int -> gt -> (gt, [Int])
randomNumbersInRange range count gen0 =
    if (count <= 0)
        then (gen0, [])
        else
            let (v0, rng1)   =  randomR range gen0
                (rng2, rest) =  randomNumbersInRange range (count-1) rng1
            in
                (rng2, v0 : rest)

Монадический стиль:

Объект монадического действия для N выходных значений очень просто написать:

import  System.Random
import  Control.Monad.Random

mkRandSeqM :: (RandomGen tg, Random tv) => (tv,tv) -> Int -> Rand tg [tv]
mkRandSeqM range count = sequence (replicate count (getRandomR range))

и чтобы использовать действие, вам просто нужно передать его в библиотечную функцию runRand.

Тестирование под ghci:

 λ> 
 λ> action = mkRandSeqM (0,9) 20
 λ> 
 λ> :type (runRand action (mkStdGen 43))
(runRand action (mkStdGen 43))
  :: (Random tv, Num tv) => ([tv], StdGen)
 λ> 
 λ> runRand action (mkStdGen 43)
([3,3,7,8,1,9,1,1,5,3,1,2,6,7,4,1,7,8,1,6],1270926008 238604751)
 λ> 

Примечание. Остерегайтесь, что пакет Haskell System.Random недавно подвергся значительным изменениям, что привело к версия 1.2.

Философия изменений здесь например. Надеюсь на обратную совместимость. Возможно, вы захотите проверить свой уровень распространения.

person jpmarinier    schedule 19.03.2021
comment
У меня сильное отвращение к написанию любого кода для создания списков, который сам производит подсчет. почему я должен знать заранее, как можно производить предметы? вместо этого легко изменить свой код (и мой), чтобы вместо этого создавать [(Double, StdGen)], и передавать его через map fst, take n, snd . last и что у вас есть. также, с State Monad мы имеем все это уже как sequence, evalState и execState. - person Will Ness; 19.03.2021
comment
@WillNess - возможно, вам не нужно «знать» точное количество предметов, просто оно должно быть намного больше, чем (например) 1000. Скажем, для вашей симуляции Монте-Карло требуется 100 миллионов тепловых нейтронов, следовательно, один миллиард случайных гауссовских переменных. Он будет работать на 256 ядрах, следовательно, 400 000 случайных нейтронов на ядро. Как только вы начнете генерировать случайные нейтроны, имеет смысл создать и буферизовать партию из (как минимум) 1000 из них, среди прочего, ради локальности кэша инструкций. В лучшем случае вы можете оставить впустую в среднем 5000 гауссовских переменных, стоимость которых незначительна. - person jpmarinier; 19.03.2021
comment
интересно, спасибо за разъяснение! - person Will Ness; 20.03.2021