Ленивое дерево с утечкой пространства

Я пишу программу, пытающуюся реализовать игрушечный XML-процессор. Прямо сейчас программа должна читать поток событий (думаю SAX), описывая структуру документа и лениво строить соответствующее дерево.

События определяются следующим типом данных:

data Event = Open String
           | Close

Возможный ввод:

[Open "a", Open "b", Close, Open "c", Close, Close]

который будет соответствовать дереву:

  a
 / \
b   c

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

data Event = Open String
           | Close

data Tree a = Tree a (Trees a)

type Trees a = [Tree a]

data Node = Node String


trees [] = []
trees (Open x : es) =
    let (children, rest) = splitStream es
    in  (Tree (Node x) (trees children)) : (trees rest)

splitStream es = scan 1 es

scan depth ([email protected](Open {}) : ss) =
    let (b, a) = scan (depth+1) ss
    in  (s:b, a)
scan depth ([email protected] : ss) =
    case depth of
      1 -> ([], ss)
      x -> let (b, a) = scan (depth-1) ss
           in  (s:b, a)


getChildren = concatMap loop
  where
    loop (Tree _ cs) = cs


main = print .
       length .
       getChildren .
       trees $
       [Open "a"] ++ (concat . replicate 1000000 $ [Open "b", Close]) ++ [Close]

Функция trees преобразует список событий в список Tree Node. getChildren собирает все дочерние узлы (помеченные как "b" ) корня ( "a" ). Затем они подсчитываются и выводится результирующее число.

Скомпилированная программа, построенная с помощью GHC 7.0.4 (-O2), продолжает увеличивать объем использования памяти до момента, когда печатает счетчик node. С другой стороны, я ожидал почти постоянного использования памяти.

Глядя на профиль "-hd" кучи, ясно, что большую часть памяти занимает конструктор списка (:). Кажется, что один из списков, созданных scan или trees, сохраняется полностью. Однако я не понимаю, почему, поскольку length . getChildren должен избавиться от дочерних узлов, как только они пройдут.

Есть ли способ исправить такую ​​утечку пространства?

Ответ 1

Я подозреваю, что trees - злой парень. Поскольку Джон Л сказал, что это, вероятно, экземпляр Wadler Space Leak, в котором компилятор не может применить оптимизацию, которая предотвращает утечку. Проблема заключается в том, что вы используете ленивое сопоставление шаблонов (выражение let) для деконструкции пары и выполнения сопоставления шаблонов с помощью приложения trees на одном из компонентов кортежа. У меня была аналогичная проблема, когда-то http://comments.gmane.org/gmane.comp.lang.haskell.glasgow.user/19129. Эта тема также дает более подробное объяснение. Чтобы предотвратить утечку пространства, вы можете просто использовать выражение case для деконструирования кортежа следующим образом.

trees [] = []
trees (Open x : es) =
  case splitStream es of
       (children, rest) -> Tree (Node x) (trees children) : trees rest

При такой реализации максимальное место жительства уменьшается от 38 МБ до 28 КБ.

Но обратите внимание, что эта новая реализация trees более строгая, чем оригинальная, поскольку она требует применения splitStream. Поэтому в некоторых случаях это преобразование может даже вызвать утечку пространства. Чтобы восстановить менее строгую реализацию, вы можете использовать подобный трюк, как функция lines в Data.List, которая вызывает аналогичную проблему http://hackage.haskell.org/packages/archive/base/latest/doc/html/src/Data-List.html#lines. В этом случае trees будет выглядеть следующим образом.

trees [] = []
trees (Open x : es) =
  context (case splitStream es of
                (children, rest) -> (trees children, trees rest))
 where
   context ~(children', rest') = Tree (Node x) children' : rest'

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

trees [] = []
trees (Open x : es) = Tree (Node x) children' : rest'
 where
  (children', rest') =
     case splitStream es of
          (children, rest) -> (trees children, trees rest)

Кто-нибудь знает, всегда ли это преобразование делает трюк?

Ответ 2

Я сильно подозреваю, что это пример ошибки "Wadler space leak" . К сожалению, я не знаю, как это решить, но я нашел несколько вещей, которые немного смягчают эффекты:

1) Измените getChildren на

getChildren' = ($ []) . foldl (\ xsf (Tree _ cs) -> xsf . (cs ++)) id

Это небольшое, но заметное улучшение.

2) В этом примере trees всегда выводит список из одного элемента. Если это всегда верно для ваших данных, явное удаление остальной части списка устраняет утечку пространства:

main = print .
   length .
   getChildren .
   (:[]) .
   head .
   trees