Разве можно развестись ленивым, широким первым монадическим розовым деревом?

Data.Tree включает функции unfoldTreeM_BF и unfoldForestM_BF для построения деревьев в ширину, сначала используя результаты монадических действий. Дерево unfolder можно легко написать с помощью лесной папки, поэтому я сосредоточусь на последнем:

unfoldForestM_BF :: Monad m =>
             (b -> m (a, [b])) -> [b] -> m [Tree a]

Начиная со списка семян, он применяет функцию к каждому, генерируя действия, которые будут генерировать корни дерева и семена для следующего уровня разворачивания. Используемый алгоритм несколько строгий, поэтому использование unfoldForestM_BF с монадой Identity не совсем то же самое, что использовать чистую unfoldForest. Я пытался выяснить, есть ли способ сделать это ленивым, не жертвуя временной привязкой O(n). Если (как мне предложил Эдвард Кемт) это невозможно, мне интересно, можно ли сделать это с более ограниченным типом, в частности требуя MonadFix, а не Monad. Концепция там была бы (каким-то образом) настроить указатели на результаты будущих вычислений при добавлении этих вычислений в список дел, поэтому, если они ленивы в эффектах более ранних вычислений, они будут доступны сразу.

Ответ 1

Ранее я утверждал, что третье решение, представленное ниже, имеет ту же строгость, что и первая глубина unfoldForest, что неверно.

Ваша интуиция в том, что деревья можно лениво развернуть по ширине, по крайней мере частично исправлена, даже если нам не нужен экземпляр MonadFix. Решения существуют для особых случаев, когда известно, что коэффициент ветвления является конечным, и когда коэффициент ветвления известен как "большой". Начнем с решения, которое выполняется в O(n) времени для деревьев с конечными ветвящимися факторами, включая вырожденные деревья с одним ребенком на node. Решение для конечных факторов ветвления не будет завершено на деревьях с бесконечными факторами ветвления, которые мы исправим решением, которое выполняется в O(n) времени для деревьев с "большими" факторами ветвления, большими, чем единицы, включая деревья с бесконечным коэффициентом ветвления. Решение для "больших" коэффициентов ветвления будет выполняться в O(n^2) времени на вырожденных деревьях с одним ребенком или без детей на node. Когда мы комбинируем методы с обоих этапов в попытке сделать гибридное решение, которое выполняется в O(n) времени для любого коэффициента ветвления, мы получим решение, которое будет более лёгким, чем первое решение для конечных факторов ветвления, но не может вместить деревья, которые создают быстрый переход от бесконечного фактора ветвления к отсутствию ветвей.

Конечный ветвящийся фактор

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

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

import Data.Tree hiding (unfoldTreeM_BF, unfoldForestM_BF)

unfoldForestM_BF :: Monad m => (b->m (a, [b])) -> [b] -> m [Tree a]
unfoldForestM_BF f []    = return []
unfoldForestM_BF f seeds = do
    level <- sequence . fmap f $ seeds
    let (labels, bs) = unzip level
    deeper <- unfoldForestM_BF f (concat bs)
    let forests = trace bs deeper
    return $ zipWith Node labels forests

trace восстанавливает структуру вложенных списков из сплющенного списка. Предполагается, что в [b] есть элемент в каждом элементе в [[a]]. Использование concat... trace, чтобы сгладить всю информацию о уровнях предков, не позволяет этой реализации работать с деревьями с бесконечными дочерними элементами для node.

trace :: [[a]] -> [b] -> [[b]]
trace []       ys = []
trace (xs:xxs) ys =
    let (ys', rem) = takeRemainder xs ys
    in   ys':trace xxs rem
    where
        takeRemainder []        ys  = ([], ys)
        takeRemainder (x:xs) (y:ys) = 
            let (  ys', rem) = takeRemainder xs ys
            in  (y:ys', rem)

Развертывание дерева тривиально писать в терминах разворачивания леса.

unfoldTreeM_BF :: MonadFix m => (b->m (a, [b])) -> b -> m (Tree a)
unfoldTreeM_BF f = (>>= return . head) . unfoldForestMFix_BF f . (:[])

Большой коэффициент ветвления

Решение для большого коэффициента ветвления происходит во многом так же, как решение для конечного фактора ветвления, за исключением того, что он удерживает всю структуру дерева вместо concat, вставляя ветки на уровне в один список и trace содержащий этот список. В дополнение к import, используемому в предыдущем разделе, мы будем использовать Compose для компоновки функторов для нескольких уровней дерева вместе и от Traversable до sequence для многоуровневых структур.

import Data.Tree hiding (unfoldForestM_BF, unfoldTreeM_BF)

import Data.Foldable
import Data.Traversable
import Data.Functor.Compose
import Prelude hiding (sequence, foldr)

Вместо того, чтобы сплющить все структуры предков вместе с concat, мы обернем с Compose предками и семенами для следующего уровня и рекурсируем по всей структуре.

unfoldForestM_BF :: (Traversable t, Traceable t, Monad m) =>
                    (b->m (a, [b])) -> t b -> m (t (Tree a))
unfoldForestM_BF f seeds
    | isEmpty seeds = return (fmap (const undefined) seeds)
    | otherwise     = do
        level <- sequence . fmap f $ seeds
        deeper <- unfoldForestM_BF f (Compose (fmap snd level))
        return $ zipWithIrrefutable Node (fmap fst level) (getCompose deeper)

zipWithIrrefutable является более ленивой версией zipWith, которая полагается на предположение, что во втором списке есть элемент во втором элементе в первом списке. Структуры Traceable представляют собой Functors, которые могут обеспечить zipWithIrrefutable. Законы для Traceable для каждого a, xs и ys, если fmap (const a) xs == fmap (const a) ys, затем zipWithIrrefutable (\x _ -> x) xs ys == xs и zipWithIrrefutable (\_ y -> y) xs ys == ys. Его строгость дается для всех f и xs на zipWithIrrefutable f xs ⊥ == fmap (\x -> f x ⊥) xs.

class Functor f => Traceable f where
    zipWithIrrefutable :: (a -> b -> c) -> f a -> f b -> f c 

Мы можем совместить два списка лениво, если мы уже знаем, что они имеют одинаковую структуру.

instance Traceable [] where
    zipWithIrrefutable f []       ys    = []
    zipWithIrrefutable f (x:xs) ~(y:ys) = f x y : zipWithIrrefutable f xs ys 

Мы можем комбинировать состав двух функторов, если мы знаем, что мы можем объединить каждый функтор.

instance (Traceable f, Traceable g) => Traceable (Compose f g) where
    zipWithIrrefutable f (Compose xs) (Compose ys) =
        Compose (zipWithIrrefutable (zipWithIrrefutable f) xs ys)

isEmpty проверяет, что пустая структура узлов расширяется, как совпадение шаблона на [], в решении для конечных факторов ветвления.

isEmpty :: Foldable f => f a -> Bool
isEmpty = foldr (\_ _ -> False) True

Проницательный читатель может заметить, что zipWithIrrefutable из Traceable очень похож на liftA2, который является половиной определения Applicative.

Гибридное решение

Гибридное решение объединяет подходы конечного решения и "большого" решения. Как и конечное решение, мы будем сжимать и декомпрессировать представление дерева на каждом шаге. Подобно решению для "больших" факторов ветвления, мы будем использовать структуру данных, которая позволяет переходить через полные ветки. Для решения конечного разветвляющего фактора использовался тип данных, который сглаживался везде, [b]. В "большом" разветвляющем коэффициенте используется тип данных, который нигде не был сплющен: все больше вложенных списков, начиная с [b], затем [[b]], затем [[[b]]] и т.д. Между этими структурами будут вложенные списки, которые либо остановят вложенность, либо просто удерживают b или удерживают вложенность и удерживают [b] s. Эта схема рекурсии описывается в основном монадой Free.

data Free f a = Pure a | Free (f (Free f a))

Мы будем работать специально с Free [], который выглядит.

data Free [] a = Pure a | Free [Free [] a]

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

import Data.Tree hiding (unfoldTreeM_BF, unfoldForestM_BF)

import Data.Traversable
import Prelude hiding (sequence, foldr)

Поскольку мы будем работать с Free [], мы предоставим ему zipWithIrrefutable.

class Functor f => Traceable f where
    zipWithIrrefutable :: (a -> b -> c) -> f a -> f b -> f c  

instance Traceable [] where
    zipWithIrrefutable f []       ys    = []
    zipWithIrrefutable f (x:xs) ~(y:ys) = f x y : zipWithIrrefutable f xs ys 

instance (Traceable f) => Traceable (Free f) where
    zipWithIrrefutable f (Pure x)  ~(Pure y ) = Pure (f x y)
    zipWithIrrefutable f (Free xs) ~(Free ys) =
        Free (zipWithIrrefutable (zipWithIrrefutable f) xs ys)

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

unfoldFreeM_BF :: (Monad m) => (b->m (a, [b])) -> Free [] b -> m (Free [] (Tree a))
unfoldFreeM_BF f (Free []) = return (Free [])
unfoldFreeM_BF f seeds = do
    level <- sequence . fmap f $ seeds
    let (compressed, decompress) = compress (fmap snd level)
    deeper <- unfoldFreeM_BF f compressed
    let forests = decompress deeper
    return $ zipWithIrrefutable Node (fmap fst level) forests

compress берет Free [], удерживая семена для лесов [b] и выравнивает [b] в Free, чтобы получить Free [] b. Он также возвращает функцию decompress, которая может использоваться для отмены выравнивания, чтобы вернуть исходную структуру. Мы сжимаем ветки без оставшихся семян и ветвей, которые только ветвятся в одну сторону.

compress :: Free [] [b] -> (Free [] b, Free [] a -> Free [] [a])
compress (Pure [x]) = (Pure x, \(Pure x) -> Pure [x])
compress (Pure xs ) = (Free (map Pure xs), \(Free ps) -> Pure (map getPure ps))
compress (Free xs)  = wrapList . compressList . map compress $ xs
    where    
        compressList []                 = ([], const [])
        compressList ((Free [],dx):xs) = let (xs', dxs) = compressList xs
                                         in  (xs', \xs -> dx (Free []):dxs xs)
        compressList (      (x,dx):xs) = let (xs', dxs) = compressList xs
                                         in  (x:xs', \(x:xs) -> dx x:dxs xs)
        wrapList ([x], dxs) = (x,             \x   -> Free (dxs [x]))
        wrapList (xs , dxs) = (Free xs, \(Free xs) -> Free (dxs xs ))

Каждый шаг сжатия также возвращает функцию, которая отменяет ее при применении к дереву Free [] с той же структурой. Все эти функции частично определены; то, что они делают с деревьями Free [] с другой структурой, undefined. Для простоты мы также определяем частичные функции для инверсий Pure и Free.

getPure (Pure x)  = x
getFree (Free xs) = xs

Оба unfoldForestM_BF и unfoldTreeM_BF определяются путем упаковки их аргумента до Free [] b и распаковки результатов, предполагая, что они находятся в одной структуре.

unfoldTreeM_BF :: MonadFix m => (b->m (a, [b])) -> b -> m (Tree a)
unfoldTreeM_BF f = (>>= return . getPure) . unfoldFreeM_BF f . Pure


unfoldForestM_BF :: MonadFix m => (b->m (a, [b])) -> [b] -> m [Tree a]
unfoldForestM_BF f = (>>= return . map getPure . getFree) . unfoldFreeM_BF f . Free . map Pure

Более элегантная версия этого алгоритма, вероятно, может быть достигнута путем распознавания того, что >>= для a Monad выполняет трансплантацию на деревьях, а оба Free и FreeT предоставляют экземпляры монады. И compress, и compressList, вероятно, имеют более элегантные презентации.

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

counterExample :: Int -> (Int, [Int])
counterExample 0 = (0, [1, 2])
counterExample 1 = (1, repeat 3)
counterExample 2 = (2, [3])
counterExample 3 = (3, [])

Это дерево будет выглядеть как

0
|
+- 1
|  |
|  +- 3
|  |
|  `- 3
|  |
|  ...
|
`- 2
   |
   +- 3

Попытка спуститься во вторую ветвь (до 2) и проверить оставшееся конечное поддерево не завершится.

Примеры

Следующие примеры демонстрируют, что все реализации unfoldForestM_BF запускают действия в первом порядке по ширине и что runIdentity . unfoldTreeM_BF (Identity . f) имеет ту же строгость, что и unfoldTree для деревьев с конечным коэффициентом ветвления. Для деревьев с фактором inifinite разветвления только решение для "больших" ветвящихся факторов имеет ту же строгость, что и unfoldTree. Чтобы продемонстрировать лень, мы определим три бесконечных дерева - унарное дерево с одной ветвью, двоичное дерево с двумя ветвями и бесконечное дерево с бесконечным количеством ветвей для каждого node.

mkUnary :: Int -> (Int, [Int])
mkUnary x = (x, [x+1])

mkBinary :: Int -> (Int, [Int])
mkBinary x = (x, [x+1,x+2])

mkInfinitary :: Int -> (Int, [Int])
mkInfinitary x = (x, [x+1..])

Вместе с unfoldTree мы определим unfoldTreeDF в терминах unfoldTreeM, чтобы проверить, что unfoldTreeM действительно ленив, как вы утверждали, и unfoldTreeBF в терминах unfoldTreeMFix_BF, чтобы проверить, что новая реализация так же ленивы.

import Data.Functor.Identity

unfoldTreeDF f = runIdentity . unfoldTreeM    (Identity . f)
unfoldTreeBF f = runIdentity . unfoldTreeM_BF (Identity . f)

Чтобы получить конечные куски этих бесконечных деревьев, даже бесконечно ветвящихся, мы определим способ взять из дерева, пока его метки соответствуют предикату. Это может быть написано более кратко с точки зрения возможности применения функции к каждому subForest.

takeWhileTree :: (a -> Bool) -> Tree a -> Tree a
takeWhileTree p (Node label branches) = Node label (takeWhileForest p branches)

takeWhileForest :: (a -> Bool) -> [Tree a] -> [Tree a]
takeWhileForest p = map (takeWhileTree p) . takeWhile (p . rootLabel)

Это позволяет нам определить девять деревьев примеров.

unary   = takeWhileTree (<= 3) (unfoldTree   mkUnary 0)
unaryDF = takeWhileTree (<= 3) (unfoldTreeDF mkUnary 0)
unaryBF = takeWhileTree (<= 3) (unfoldTreeBF mkUnary 0)

binary   = takeWhileTree (<= 3) (unfoldTree   mkBinary 0)
binaryDF = takeWhileTree (<= 3) (unfoldTreeDF mkBinary 0)
binaryBF = takeWhileTree (<= 3) (unfoldTreeBF mkBinary 0)

infinitary   = takeWhileTree (<= 3) (unfoldTree   mkInfinitary 0)
infinitaryDF = takeWhileTree (<= 3) (unfoldTreeDF mkInfinitary 0)
infinitaryBF = takeWhileTree (<= 3) (unfoldTreeBF mkInfinitary 0)

Все пять методов имеют одинаковый вывод для унарных и двоичных деревьев. Выходной сигнал поступает от putStrLn . drawTree . fmap show

0
|
`- 1
   |
   `- 2
      |
      `- 3

0
|
+- 1
|  |
|  +- 2
|  |  |
|  |  `- 3
|  |
|  `- 3
|
`- 2
   |
   `- 3

Однако ширина первого обхода от конечного разветвляющего фактора не достаточно ленива для дерева с бесконечным коэффициентом ветвления. Остальные четыре метода выводят все дерево

0
|
+- 1
|  |
|  +- 2
|  |  |
|  |  `- 3
|  |
|  `- 3
|
+- 2
|  |
|  `- 3
|
`- 3

Дерево, сгенерированное с помощью unfoldTreeBF для решения конечного разветвляющего фактора, никогда не может быть полностью выполнено за его первые ветки.

0
|
+- 1
|  |
|  +- 2
|  |  |
|  |  `- 3
|  |
|  `- 3

Конструкция определенно широта.

mkDepths :: Int -> IO (Int, [Int])
mkDepths d = do
    print d
    return (d, [d+1, d+1])

mkFiltered :: (Monad m) => (b -> Bool) -> (b -> m (a, [b])) -> (b -> m (a, [b]))
mkFiltered p f x = do
    (a, bs) <- f x
    return (a, filter p bs)

binaryDepths = unfoldTreeM_BF (mkFiltered (<= 2) mkDepths) 0

Запуск binaryDepths выводит внешние уровни перед внутренними

0
1
1
2
2
2
2

От ленивого к прямому ленивому

Гибридное решение из предыдущего раздела не достаточно ленив, чтобы иметь такую ​​же строгость семантики, как Data.Tree unfoldTree. Это первый в серии алгоритмов, каждый немного ленивый, чем их предшественник, но не достаточно ленив, чтобы иметь такую ​​же строгость семантики, как unfoldTree.

Гибридное решение не дает гарантии, что изучение части дерева не требует изучения других частей одного и того же дерева. Не будет представлен ниже код. В одном конкретном, но часто встречающемся случае идентифицированном dfeuer, изучение только размера log(N) размера конечного дерева заставляет полностью дерево. Это происходит при изучении последнего потомка каждой ветки дерева с постоянной глубиной. При сжатии дерева мы выбрасываем каждую тривиальную ветвь без потомков, что необходимо, чтобы избежать O(n^2) времени выполнения. Мы можем только лениво пропустить эту часть сжатия, если мы можем быстро показать, что ветвь имеет хотя бы одного потомка, и поэтому мы можем отказаться от шаблона Free []. На самой большой глубине дерева с постоянной глубиной ни одна из ветвей не имеет оставшихся потомков, поэтому мы никогда не сможем пропустить шаг сжатия. Это приводит к изучению всего дерева, чтобы иметь возможность посещать последний node. Когда все дерево на эту глубину не является конечным из-за бесконечного фактора ветвления, исследование части дерева не может завершиться, когда оно завершится при создании unfoldTree.

Шаг сжатия в секции гибридного решения сжимает ветки без потомков в первом поколении, которые они могут быть обнаружены, что является оптимальным для сжатия, но не оптимальным для лени. Мы можем сделать алгоритм более лёгким, задерживаясь при этом сжатии. Если мы отложим его на одно поколение (или даже любое постоянное число поколений), мы будем поддерживать верхнюю границу O(n) во времени. Если мы отложим его на несколько поколений, которые каким-то образом зависят от N, мы обязательно пожертвуем временной привязкой O(n). В этом разделе мы задерживаем сжатие одним поколением.

Чтобы контролировать, как происходит сжатие, мы отделим заполнение самой внутренней [] в структуре Free [] от сдавливания вырожденных ветвей с 0 или 1 потомками.

Поскольку часть этого трюка не работает без большого количества лени в сжатии, мы будем принимать параноидальный уровень чрезмерно ленивой лени во всем мире. Если бы что-либо о результате, отличном от конструктора кортежа (,), можно было бы определить, не форсируя часть его ввода с совпадением шаблонов, мы избежим его форсирования, пока это не понадобится. Для кортежей, что-либо сопоставление шаблонов на них будет делать это лениво. Следовательно, некоторые из приведенных ниже кодов будут выглядеть как ядро ​​или хуже.

bindFreeInvertible заменяет Pure [b,...] на Free [Pure b,...]

bindFreeInvertible :: Free [] ([] b) -> (Free [] b, Free [] a -> Free [] ([] a))
bindFreeInvertible = wrapFree . go
    where
        -- wrapFree adds the {- Free -} that would have been added in both branches
        wrapFree ~(xs, dxs) = (Free xs, dxs)
        go (Pure xs) = ({- Free -} (map Pure xs), Pure . map getPure . getFree)
        go (Free xs) = wrapList . rebuildList . map bindFreeInvertible $ xs
        rebuildList = foldr k ([], const [])
        k ~(x,dx) ~(xs, dxs) = (x:xs, \(~(x:xs)) -> dx x:dxs xs)
        wrapList ~(xs, dxs) = ({- Free -} xs, \(~(Free xs)) -> Free (dxs xs)))

compressFreeList удаляет вхождения Free [] и заменяет Free [xs] на xs.

compressFreeList :: Free [] b -> (Free [] b, Free [] a -> Free [] a)
compressFreeList (Pure x) = (Pure x, id)
compressFreeList (Free xs) = wrapList . compressList . map compressFreeList $ xs
    where
        compressList = foldr k ([], const [])
        k ~(x,dx) ~(xs', dxs) = (x', dxs')
            where
                x' = case x of
                        Free []   -> xs'
                        otherwise -> x:xs'
                dxs' cxs = dx x'':dxs xs''
                    where
                        x'' = case x of
                            Free []   -> Free []
                            otherwise -> head cxs
                        xs'' = case x of
                            Free []   -> cxs
                            otherwise -> tail cxs
        wrapList ~(xs, dxs) = (xs', dxs')
            where
                xs' = case xs of
                        [x]       -> x
                        otherwise -> Free xs
                dxs' cxs = Free (dxs xs'')
                    where
                        xs'' = case xs of
                            [x]       -> [cxs]
                            otherwise -> getFree cxs

Общее сжатие не привяжет Pure [] к Free до тех пор, пока вырожденный Free не будет сжат, задерживая сжатие вырожденного Free, введенное в одно поколение для сжатия следующего поколения.

compress :: Free [] [b] -> (Free [] b, Free [] a -> Free [] [a])
compress xs = let ~(xs' , dxs' ) = compressFreeList xs
                  ~(xs'', dxs'') = bindFreeInvertible xs'
                  in (xs'', dxs' . dxs'')

Из продолженной паранойи помощники getFree и getPure также становятся неопровержимо ленивыми.

getFree ~(Free xs) = xs
getPure ~(Pure x)  = x

Это очень быстро решает проблемный пример, обнаруженный dfeuer

print . until (null . subForest) (last . subForest) $
    flip unfoldTreeBF 0 (\x -> (x, if x > 5 then [] else replicate 10 (x+1)))

Но поскольку мы только задерживали сжатие с помощью генерации 1, мы можем воссоздать точно такую ​​же проблему, если самая последняя node самой последней ветки 1 глубже, чем все остальные ветки.

print . until (null . subForest) (last . subForest) $
    flip unfoldTreeBF (0,0) (\(x,y) -> ((x,y), 
        if x==y
        then if x>5 then [] else replicate 9 (x+1, y) ++ [(x+1, y+1)]
        else if x>4 then [] else replicate 10 (x+1, y)))