Как реализовать ленивую итерацию, когда IO участвует в Haskell - PullRequest
1 голос
/ 09 февраля 2020

Я использую IO для инкапсуляции случайности. Я пытаюсь написать метод, который повторяет next функцию n раз, но функция next выдает результат, завернутый в IO из-за случайности.

По сути, моя next функция имеет эта подпись:

next :: IO Frame -> IO Frame

, и я хочу начать с начального Frame, а затем использовать тот же шаблон, что и iterate, чтобы получить список [Frame] с длиной n. По сути, я хотел бы иметь возможность написать следующее:

runSimulation :: {- parameters -} -> IO [Frame]
runSimulation {- parameters -} = do
  {- some setup -}
  sequence . take n . iterate next $ firstFrame

Где firstFrame :: IO Frame формируется путем выполнения чего-то вроде let firstFrame = return Frame x y z.

Проблема, с которой я сталкиваюсь, заключается в том, что когда Я запускаю эту функцию, она никогда не завершается, поэтому кажется, работает на бесконечном l oop (поскольку iterate создает бесконечный список).

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


(Обновление) В случае, если это полезно Вот полные определения Frame, next и runSimulation:

-- A simulation Frame encapsulates the state of the simulation at some
-- point in "time". That means it contains a list of Agents in that
-- Frame, and a list of the Interactions that occurred in it as well. It
-- also contains the state of the World, as well as an AgentID counter
-- (so we can easily increment for generating new Agents).
data Frame = Frame AgentID [Agent] [Interaction]
  deriving Show

-- Generate the next Frame from the current one, including scoring the
-- Agents based on the outcomes *in this Frame*.
-- TODO: add in reproduction.
nextFrame :: Reactor -> World -> IO Frame -> IO Frame
nextFrame react w inp = do
  (Frame i agents history) <- inp
  interactions <- interactAll react history agents
  let scoredAgents = scoreAgents (rewards w) interactions agents
  return (Frame i scoredAgents interactions)

-- Run a simulation for a number of iterations
runSimulation :: World -> Reactor -> (Dist, Dist) -> IO [Frame]
runSimulation world react (gen_dist, sel_dist) = do
  startingAgents <- spawnAgents (initial_size world) (agentCreatorFactory gen_dist sel_dist)
  let firstFrame = return (Frame (length startingAgents) startingAgents [])
      next       = nextFrame react world
  sequence . take (iterations world) . iterate next $ firstFrame

Ответы [ 2 ]

3 голосов
/ 09 февраля 2020

Я не знаю, сколько времени занимает вычисление Frame, но я подозреваю, что вы выполняете больше работы, чем необходимо. Причина немного тонкая. iterate создает список повторных применений функции. Для каждого элемента в списке предыдущее значение используется повторно. Ваш список состоит из IO действий. Действие IO в позиции n вычисляется из уже полученного действия IO в позиции n-1 путем применения next.

Увы, когда выполняя эти действия, нам не так повезло. Выполнение действия в позиции n в списке повторит всю работу предыдущих действий! Мы разделили работу при построении самих действий (которые являются значениями, как почти все в Haskell), но не при их выполнении, что совсем другое.

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

iterateM :: Monad m => (a -> m a) -> a -> Int -> m [a]
iterateM step = go
    where
    go _ 0 = return []
    go current limit =
        do next <- step current
           (current:) <$> go next (pred limit)

Простое, но немного не элегантное по двум причинам:

  • Оно связывает процесс итерации с ограничением такого процесса. , В чистом мире списков нам не нужно было этого делать, мы могли бы создавать бесконечные списки и с этого момента take. Но теперь в эффективном мире эта хорошая композиционная структура, кажется, потеряна.

  • Что, если мы хотим что-то сделать с каждым значением по мере его производства, не дожидаясь их всех? ? Функция Out возвращает все в конце, в одном go.


Как уже упоминалось в комментариях, потоковые библиотеки, такие как "pipelineit * , streamly или streaming попытайтесь решить эту проблему лучшим способом, восстановив некоторую композиционность чистых списков. В этих библиотеках есть типы, представляющие эффективные процессы, результаты которых получаются кусочно.

Например, рассмотрим функцию Streaming.Prelude.iterateM из «потокового», специализированную для IO:

iterateM :: (a -> IO a) -> IO a -> Stream (Of a) IO r

Возвращает Stream, который мы можем "ограничить", используя Streaming.Prelude.take:

take :: Int -> Stream (Of) IO r -> Stream (Of) IO ()

после ограничения мы можем вернуться к IO [a] с Streaming.Prelude.toList_, который накапливает все результаты:

toList_ :: Stream (Of) IO r -> IO [ a]

Но вместо этого мы могли бы обрабатывать каждый элемент по мере его изготовления с такими функциями, как Streaming.Prelude.mapM_:

mapM_ :: (a -> IO x) -> Поток (Of a) IO r -> IO r

1 голос
/ 11 февраля 2020

Элементарное решение:

В качестве альтернативы ответу @ danidiaz можно решить проблему, не прибегая к дополнительным библиотекам, таким как Streaming, при условии, что роль ввода-вывода может быть минимизирована.

Большая часть необходимого кода может быть написана в терминах класса MonadRandom , одним из которых является IO. Нет необходимости использовать IO для генерации псевдослучайных чисел.

Требуемая итерационная функция может быть написана так в do нотации :

import  System.Random
import  Control.Monad.Random.Lazy

iterateM1 :: MonadRandom mr => (a -> mr a) -> a -> mr [a]
iterateM1 fn x0 = 
    do
        y  <- fn x0
        ys <- iterateM1 fn y
        return (x0:ys)

К сожалению, текст вопроса не определяет точно, что такое объект Frame или что делает степпинг-функция next; поэтому я должен как-то заполнить пробелы. Также имя next определено в задействованных библиотеках, поэтому мне придется использовать nextFrame вместо просто next.

Давайте предположим, что объект Frame - это просто точка в 3-мерном пространстве. и что на каждом случайном шаге одно и только одно из трех измерений выбирается случайным образом, а соответствующая координата увеличивается на величину либо +1, либо -1 с равными вероятностями. Это дает следующий код:

data Frame = Frame Int Int Int  deriving  Show

nextFrame :: MonadRandom mr => Frame -> mr Frame
nextFrame (Frame x y z) =
    do
        -- 3 dimensions times 2 possible steps: 1 & -1, hence 6 possibilities
        n <- getRandomR (0::Int, 5::Int)
        let fr = case n of
                  0 -> Frame (x-1) y z
                  1 -> Frame (x+1) y z
                  2 -> Frame x (y-1) z
                  3 -> Frame x (y+1) z
                  4 -> Frame x y (z-1)
                  5 -> Frame x y (z+1)
                  _ -> Frame x y z
        return fr

На этом этапе нетрудно написать код, который создает неограниченный список объектов Frame, представляющих историю моделирования. Создание этого списка не приводит к тому, что код будет l oop навсегда, и обычная функция take может использоваться для выбора первых нескольких элементов такого списка.

Объединение всего кода:

import  System.Random
import  Control.Monad.Random.Lazy

iterateM1 :: MonadRandom mr => (a -> mr a) -> a -> mr [a]
iterateM1 fn x0 = 
    do
        y  <- fn x0
        ys <- iterateM1 fn y
        return (x0:ys)

data Frame = Frame Int Int Int  deriving  Show

nextFrame :: MonadRandom mr => Frame -> mr Frame
nextFrame (Frame x y z) =
    do
        -- 3 dimensions times 2 possible steps: 1 & -1, hence 6 possibilities
        n <- getRandomR (0::Int, 5::Int)
        let fr = case n of
                  0 -> Frame (x-1) y z
                  1 -> Frame (x+1) y z
                  2 -> Frame x (y-1) z
                  3 -> Frame x (y+1) z
                  4 -> Frame x y (z-1)
                  5 -> Frame x y (z+1)
                  _ -> Frame x y z
        return fr

runSimulation :: MonadRandom mr => Int -> Int -> Int -> mr [Frame]
runSimulation x y z  =  let  fr0 = Frame x y z  in  iterateM1 nextFrame fr0

main = do
    rng0 <- getStdGen  -- PRNG hosted in IO monad
                       -- Could use mkStdGen or MkTFGen instead
    let
         sim  = runSimulation 0 0 0
         allFrames = evalRand sim rng0   -- unlimited list of frames !
         frameCount = 10
         frames = take frameCount allFrames
    mapM_  (putStrLn  .  show)  frames

Выполнение программы:

$ ./frame
Frame 0 0 0
Frame 0 1 0
Frame 0 0 0
Frame 0 (-1) 0
Frame 1 (-1) 0
Frame 1 (-2) 0
Frame 1 (-1) 0
Frame 1 (-1) 1
Frame 1 0 1
Frame 2 0 1
$ 


Для больших значений frameCount время выполнения является квазилинейной функцией frameCount, как и ожидалось.

Подробнее о монади c действия для генерации случайных чисел здесь .

...