Рекурсия Haskell со случайными числами и вводом-выводом

#haskell #recursion #io

#haskell #рекурсия #io

Вопрос:

Для 99 вопросов Haskell, в частности 23-го, мне нужно

«Извлеките заданное количество случайно выбранных элементов из списка.

Пример (на lisp):

 (rnd-select '(a b c d e f g h) 3)
(E D A)
  

«

Которую я реализовал следующим образом:

 import System.Random
import Control.Monad

removeAt :: [a] -> Int -> [a]
removeAt (x:xs) i
    | i > 0  = x : removeAt xs (i-1)
    | otherwise = xs

rndSelect :: (RandomGen g) => [a] -> Int ->  g -> IO [a]
rndSelect _ 0 _ = return []
rndSelect xs n gen = do
    let (pos, newGen) = randomR (0, length xs - 1) gen
    rest <- rndSelect (removeAt xs pos) (n-1) newGen
    return $ (xs!!pos):rest

-- for an explanation of what this is doing see EXPLANATION below
  

Насколько я могу судить, это работает, но меня беспокоят эти последние две строки. Я новичок в этом, и я не знаю, какие связанные с этим затраты на оператор ‘<-‘ — это или многократное переключение ввода-вывода, как я делаю. Эффективно ли это, есть ли лучший способ сделать это, который не требует скачкообразного ввода-вывода, или нет никаких реальных накладных расходов?

Я ценю любое ваше понимание, поскольку я только недавно начал изучать эти более сложные концепции в Haskell и еще не привык рассуждать о системе ввода-вывода Haskell.

ОБЪЯСНЕНИЕ: Чтобы сделать это, я решил, что мне следует случайным образом выбрать один элемент из списка, используя функцию randomR (возвращает случайное число в заданном диапазоне), и продолжать делать это рекурсивно, пока я не возьму n элементов.

Я сделал пару предположений о проблеме, которые привели меня к этому подходу. Во-первых, я предположил, что rndSelect может выбрать определенный элемент из списка только один раз, а во-вторых, я предположил, что каждый элемент должен иметь равную вероятность выбора.

PS: это мой первый вопрос по SO, поэтому, если я плохо отформатировал вопрос, не стесняйтесь сказать мне.

Комментарии:

1. Вам действительно нужно обернуть результат в IO monad? Почему этого не может быть: rndSelect :: (RandomGen g) => [a] -> Int -> g -> [a] …. Пользователь этой функции может при необходимости обернуть список результатов в IO, используя функцию return.

2. Я использую IO, потому что это единственный известный мне способ получения случайных чисел. Я хочу случайным образом выбрать / удалить элемент из списка, затем вернуть список, чтобы я мог сделать это снова (n-1) раз. Из-за чистоты Haskell я не думаю, что смогу сделать это вне монады ввода-вывода, но, возможно, удастся написать вспомогательную функцию, которая не использует ввод-вывод.

Ответ №1:

Для этого вам не нужен IO, поскольку randomR этого не требует. Что вам нужно сделать, однако, это подключить генератор случайных чисел к вашим вычислениям:

 import System.Random
import Control.Monad

removeAt :: [a] -> Int -> [a]
removeAt (x:xs) i
    | i > 0  = x : removeAt xs (i-1)
    | otherwise = xs


rndSelect :: (RandomGen t, Num a) => [a1] -> a -> t -> ([a1], t)
rndSelect _ 0 g = ([],g)
rndSelect xs n gen =
   let (pos, newGen) = randomR (0, length xs - 1) gen
       (rest,ng)     = rndSelect (removeAt xs pos) (n-1) newGen
   in  ((xs!!pos):rest, ng)
  

Если вас беспокоят накладные расходы при переходе от ввода-вывода к чистому коду, не стоит. Вместо этого вы можете попробовать mwc-random package, который в этом случае будет по крайней мере на порядок быстрее. Кроме того, вы могли бы получить дополнительную выгоду, используя любую структуру данных с произвольным доступом вместо списка, если у вас много элементов.

Комментарии:

1. Верно, я понимаю. Я запутался и предположил, что мне нужен IO, потому что я читал о getStdGen в LYAH, когда все, что мне было нужно, это начальный randomGen в начале. > Если вас беспокоят накладные расходы при переходе от ввода-вывода к чистому коду, не стоит. Спасибо. Оптимизация этой программы была важна не столько для того, чтобы выяснить, связаны ли затраты на ввод-вывод, так что это полезно знать.

Ответ №2:

Вы можете избежать ввода-вывода как :

 rndSelect :: (RandomGen g) => [a] -> Int ->  g -> [a]
rndSelect _ 0 _ = return []
rndSelect xs n gen = do
    let (pos, newGen) = randomR (0, length xs - 1) gen
         rest = rndSelect (removeAt xs pos) (n-1) newGen
    in (xs!!pos):rest
  

Комментарии:

1. Спасибо, я только что получил ответ aleator, но вы правы в том, что я ошибался, думая, что мне нужен ввод-вывод.