Возможно ли разворачивать ленивое монадическое розовое дерево первой ширины?
Data.Tree
включает в себя unfoldTreeM_BF
а также unfoldForestM_BF
функции для построения деревьев в ширину, используя результаты монадических действий. Развертывание дерева можно легко записать с помощью развертывания леса, поэтому я сосредоточусь на последнем:
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)
время для деревьев с конечными факторами ветвления, включая вырожденные деревья только с одним потомком на узел. Решение для конечных факторов ветвления не будет заканчиваться на деревьях с бесконечными факторами ветвления, которые мы исправим решением, которое работает в O(n)
время для деревьев с "большими" факторами ветвления больше единицы, включая деревья с бесконечным фактором ветвления. Решение для "больших" факторов ветвления будет работать в O(n^2)
время на вырожденных деревьях только с одним ребенком или без детей на узел. Когда мы объединяем методы из обоих шагов в попытке сделать гибридное решение, которое работает в 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
Сглаживание всей информации об уровнях предков не позволяет этой реализации работать на деревьях с бесконечными дочерними элементами для узла.
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
S используется в предыдущем разделе, мы будем использовать 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 []
деревья с другой структурой не определены. Для простоты мы также определим частичные функции для обратных 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
Более элегантную версию этого алгоритма, вероятно, можно сделать, признав, что >>=
для 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
для деревьев с конечным коэффициентом ветвления. Для деревьев с бесконечным фактором разветвления, только решение для "больших" факторов разветвления имеет ту же строгость, что и unfoldTree
, Чтобы продемонстрировать лень, мы определим три бесконечных дерева - унарное дерево с одной ветвью, бинарное дерево с двумя ветвями и бесконечное дерево с бесконечным числом ветвей для каждого узла.
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
"s unfoldTree
, Это первый из серии алгоритмов, каждый из которых немного ленивее своего предшественника, но ни один из них не достаточно ленив, чтобы иметь ту же семантику строгости, что и unfoldTree
,
Гибридное решение не дает гарантии, что исследование части дерева не требует исследования других частей того же дерева. Также не будет код, представленный ниже. В одном конкретном, но все же частом случае, выявленном dfeuer, исследующим только log(N)
размерный кусок конечного дерева заставляет дерево целиком. Это происходит при исследовании последнего потомка каждой ветви дерева с постоянной глубиной. При сжатии дерева мы выбрасываем каждую тривиальную ветвь без потомков, чего необходимо избегать O(n^2)
Продолжительность. Мы можем только лениво пропустить эту часть сжатия, если мы можем быстро показать, что у ветви есть хотя бы один потомок, и поэтому мы можем отклонить шаблон Free []
, На самой большой глубине дерева с постоянной глубиной ни одна из ветвей не имеет оставшихся потомков, поэтому мы никогда не можем пропустить шаг сжатия. Это приводит к исследованию всего дерева, чтобы иметь возможность посетить самый последний узел. Когда все дерево на эту глубину не является конечным из-за бесконечного коэффициента ветвления, исследование части дерева не завершается, когда оно завершается, когда генерируется 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
s были сжаты, задерживая сжатие вырожденных Free
s введены в одном поколении в сжатие следующего поколения.
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
поколение, мы можем воссоздать точно такую же проблему, если самый последний узел самой последней ветви 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)))