Преобразователь отложенного состояния потребляет отложенный список в 2D-рекурсии - PullRequest
4 голосов
/ 17 февраля 2020

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

Чтобы быть конкретным, рассмотрите эту программу:

import Control.Monad ( sequence, liftM2 )
import Data.Functor.Identity
import Control.Monad.State.Lazy ( StateT(..), State(..), runState )

walk :: Int -> Int -> [State Int [Int]]
walk _ 0 = [return [0]]
walk 0 _ = [return [0]]
walk x y =
  let st :: [State Int Int]
      st = [StateT (\s -> Identity (s, s + 1)), undefined]
      unst :: [State Int Int] -- degenerate state tf
      unst = [return 1, undefined]
  in map (\m_z -> do
      z <- m_z
      fmap concat $ sequence [
          liftM2 (zipWith (\x y -> x + y + z)) a b -- for 1D: map (+z) <$> a
          | a <- walk x (y - 1) -- depth
          , b <- walk (x - 1) y -- breadth -- comment out for 1D
        ]
    ) st -- vs. unst

main :: IO ()
main = do
  std <- getStdGen
  putStrLn $ show $ head $ fst $ (`runState` 0) $ head $ walk 2 2

Программа проходит по прямоугольной сетке angular от (x, y) до (0, 0) и суммирует все результаты, включая значение одного из списков монад состояний: либо нетривиальные преобразователи st, которые читают и продвигать свое состояние или тривиальные трансформаторы unst. Интересно, исследует ли алгоритм последние главы st и unst.

В представленном коде он выдает undefined. Я объяснил это неправильным дизайном моего порядка связывания преобразований и, в частности, проблемой с обработкой состояний, так как вместо этого используется unst (то есть отделение результата от переходов состояний), которое дает результат. Однако затем я обнаружил, что 1D-рекурсия также сохраняет лень даже с преобразователем состояния (уберите шаг ширины b <- walk... и поменяйте блок liftM2 на fmap).

Если мы trace (show (x, y)), мы также видим, что он обходит всю сетку перед запуском:

$ cabal run
Build profile: -w ghc-8.6.5 -O1
...
(2,2)
(2,1)
(1,2)
(1,1)
(1,1)
sandbox: Prelude.undefined

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

Что вызывает здесь разницу в строгости между 1D и 2D рекурсией и как я могу достичь лени, которую я хочу?

Ответы [ 2 ]

2 голосов
/ 08 марта 2020

Рассмотрим следующий упрощенный пример:

import Control.Monad.State.Lazy

st :: [State Int Int]
st = [state (\s -> (s, s + 1)), undefined]

action1d = do
  a <- sequence st
  return $ map (2*) a

action2d = do
  a <- sequence st
  b <- sequence st
  return $ zipWith (+) a b

main :: IO ()
main = do
  print $ head $ evalState action1d 0
  print $ head $ evalState action2d 0

Здесь, как в 1D, так и в 2D вычислениях, заголовок результата явно зависит только от заголовков входов (просто head a для 1D действие и head a и head b для 2D-действия). Однако в 2D-расчете существует неявная зависимость b (даже только его голова) от текущего состояния, и это состояние зависит от оценки полноты of a, а не только его голова.

У вас есть похожая зависимость в вашем примере, хотя она скрыта использованием списков действий состояния.

Допустим, мы хотели запустить действие walk22_head = head $ walk 2 2 вручную и проверьте первое целое число в результирующем списке:

main = print $ head $ evalState walk22_head

Записав элементы списка состояний состояния st явно:

st1, st2 :: State Int Int
st1 = state (\s -> (s, s+1))
st2 = undefined

мы можем написать walk22_head as:

walk22_head = do
  z <- st1
  a <- walk21_head
  b <- walk12_head
  return $ zipWith (\x y -> x + y + z) a b

Обратите внимание, что это зависит только от определенного состояния действия st1 и головок walk 2 1 и walk 1 2. Эти главы, в свою очередь, можно записать так:

walk21_head = do
  z <- st1
  a <- return [0] -- walk20_head
  b <- walk11_head
  return $ zipWith (\x y -> x + y + z) a b

walk12_head = do
  z <- st1
  a <- walk11_head
  b <- return [0] -- walk02_head
  return $ zipWith (\x y -> x + y + z) a b

Опять же, они зависят только от определенного состояния действия st1 и главы walk 1 1.

Теперь давайте попробуем записать определение walk11_head:

walk11_head = do
  z <- st1
  a <- return [0]
  b <- return [0]
  return $ zipWith (\x y -> x + y + z) a b

Это зависит только от действия определенного состояния st1, поэтому при наличии этих определений, если мы запустим main, мы получим определенный ответ :

> main
10

Но эти определения не точны! В каждом из walk 1 2 и walk 2 1 действие головы представляет собой последовательность действий, начиная с действия, которое вызывает walk11_head, но продолжая действиями, основанными на walk11_tail. Итак, более точные определения были бы:

walk21_head = do
  z <- st1
  a <- return [0] -- walk20_head
  b <- walk11_head
  _ <- walk11_tail  -- side effect of the sequennce
  return $ zipWith (\x y -> x + y + z) a b

walk12_head = do
  z <- st1
  a <- walk11_head
  b <- return [0] -- walk02_head
  _ <- walk11_tail  -- side effect of the sequence
  return $ zipWith (\x y -> x + y + z) a b

с:

walk11_tail = do
  z <- undefined
  a <- return [0]
  b <- return [0]
  return [zipWith (\x y -> x + y + z) a b]

С этими определениями нет проблем при выполнении walk12_head и walk21_head в изоляции:

> head $ evalState walk12_head 0
1
> head $ evalState walk21_head 0
1

Побочные эффекты состояния здесь не нужны для вычисления ответа и поэтому никогда не вызываются. Но невозможно запустить их оба в последовательности:

> head $ evalState (walk12_head >> walk21_head) 0
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:78:14 in base:GHC.Err
  undefined, called at Lazy2D_2.hs:41:8 in main:Main

Поэтому попытка запустить main не удалась по той же причине:

> main
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:78:14 in base:GHC.Err
  undefined, called at Lazy2D_2.hs:41:8 in main:Main

, потому что при расчете walk22_head, даже самое начало вычисления walk21_head зависит от побочного эффекта состояния walk11_tail, инициируемого walk12_head.

Ваше первоначальное определение walk ведет себя так же, как эти макеты:

> head $ evalState (head $ walk 1 2) 0
1
> head $ evalState (head $ walk 2 1) 0
1
> head $ evalState (head (walk 1 2) >> head (walk 2 1)) 0
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:78:14 in base:GHC.Err
  undefined, called at Lazy2D_0.hs:15:49 in main:Main
> head $ evalState (head (walk 2 2)) 0
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:78:14 in base:GHC.Err
  undefined, called at Lazy2D_0.hs:15:49 in main:Main

Сложно сказать, как это исправить. Ваш игрушечный пример был превосходен для иллюстрации проблемы, но не ясно, как состояние используется в вашей «реальной» проблеме, и если head $ walk 2 1 действительно имеет зависимость состояния от действий sequence из walk 1 1, вызванных head $ walk 1 2.

1 голос
/ 12 марта 2020

Принятый ответ К. А. Бура верен: при правильном получении головки одного шага в каждом направлении (попробуйте walk с x < 2 или y < 2) комбинацию неявного >>= в liftM2, sequence в значении a и зависимость состояния в значении b делает b зависимым от всех побочных эффектов a. Как он также указал, рабочее решение зависит от того, какие зависимости действительно нужны.

Я поделюсь решением для моего конкретного случая: каждый вызов walk зависит, по крайней мере, от состояния вызывающего абонента, и возможно, некоторые другие состояния, основанные на обходе сетки по предварительному заказу и альтернативах в st. Кроме того, как следует из вопроса, я хочу попытаться получить полный результат, прежде чем тестировать ненужные альтернативы в st. Это немного сложно объяснить визуально, но вот лучшее, что я мог сделать: слева показано переменное число st альтернатив для каждой координаты (что у меня есть в моем реальном случае использования), а справа показано [довольно грязный] карта желаемого порядка зависимости состояния: мы видим, что сначала он проходит xy в 3D DFS, где «x» - глубина (самая быстрая ось), «y» - ширина (средняя ось), а затем, наконец, альтернативы как самая медленная ось (показана пунктирными линиями с незакрашенными кружками).

enter image description here

Центральная проблема в исходной реализации возникла из-за упорядочения списков переходов состояний для размещения не рекурсивный тип возврата. Давайте полностью заменим тип списка на тип, который является рекурсивным в параметре monad, чтобы вызывающий мог лучше контролировать порядок зависимости:

data ML m a = MCons a (MML m a) | MNil -- recursive monadic list
newtype MML m a = MML (m (ML m a)) -- base case wrapper

Пример [1, 2]:

MCons 1 (MML (return (MCons 2 (MML (return MNil)))))
* 1032 Поведения Functor и Monoid используются часто, поэтому вот соответствующие реализации:
instance Functor m => Functor (ML m) where
  fmap f (MCons a m) = MCons (f a) (MML $ (fmap f) <$> coerce m)
  fmap _ MNil = MNil

instance Monad m => Semigroup (MML m a) where
  (MML l) <> (MML r) = MML $ l >>= mapper where
    mapper (MCons la lm) = return $ MCons la (lm <> (MML r))
    mapper MNil = r

instance Monad m => Monoid (MML m a) where
  mempty = MML (pure MNil)

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

  1. Исходя из диаграммы, мы хотим сначала получить один полный результат из шага x, а затем полный результат из шага y. Каждый шаг возвращает список результатов из всех комбинаций жизнеспособных альтернатив из внутренних координат, поэтому мы берем декартово произведение по обоим спискам, также смещенным в одном направлении (в данном случае y быстрее всего). Сначала мы определяем «конкатенацию», которая применяет оболочку базового случая MML в конце пустого списка ML:

    nest :: Functor m => MML m a -> ML m a -> ML m a
    nest ma (MCons a mb) = MCons a (MML $ nest ma <$> coerce mb)
    

    , затем декартово произведение:

    prodML :: Monad m => (a -> a -> a) -> ML m a -> ML m a -> ML m a
    prodML f x (MCons ya ym) = (MML $ prodML f x <$> coerce ym) `nest` ((f ya) <$> x)
    prodML _ MNil _ = MNil
    
  2. Мы хотим объединить списки из разных альтернатив в один список, и нам все равно, что это вводит зависимости между альтернативами. Здесь мы используем mconcat из экземпляра Monoid.

В целом, это выглядит так:

walk :: Int -> Int -> MML (State Int) Int
-- base cases
walk _ 0 = MML $ return $ MCons 1 (MML $ return MNil)
walk 0 _ = walk 0 0

walk x y =
  let st :: [State Int Int]
      st = [StateT (\s -> Identity (s, s + 1)), undefined]
      xstep = coerce $ walk (x-1) y
      ystep = coerce $ walk x (y-1)
     -- point 2: smash lists with mconcat
  in mconcat $ map (\mz -> MML $ do
      z <- mz
                              -- point 1: product over results
      liftM2 ((fmap (z+) .) . prodML (+)) xstep ystep
    ) st

headML (MCons a _) = a
headML _ = undefined

main :: IO ()
main = putStrLn $ show $ headML $ fst $ (`runState` 0) $ (\(MML m) -> m) $ walk 2 2

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

(я буду также предупреждаем, что без запоминания или внимания к строгости эта реализация очень неэффективна для больших x и y.)

...