Как закодировать дерево объектов в Haskell с указателями на родителя и потомка?

У меня следующая проблема: у меня есть дерево объектов разных классов, где действие в дочернем классе делает недействительным родителя. В императивных языках это тривиально. Например, в Java:

public class A {
    private List<B> m_children = new LinkedList<B>();
    private boolean m_valid = true;

    public void invalidate() {
        m_valid = false;
    }

    public void addChild(B child) {
        m_children.add(child);
        child.m_parent = this;
    }
}

public class B {
    public A m_parent = null;
    private int m_data = 0;

    public void setData(int data) {
        m_data = 0;
        m_parent.invalidate();
    }
}

public class Main {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        b.setData(0); //invalidates A
    }
}

Как мне сделать это в Haskell? Я не могу обдумать это, поскольку, как только я создам объект в Haskell, его нельзя будет изменить.

Я был бы очень признателен, если бы соответствующий код Haskell был опубликован.

РЕДАКТИРОВАТЬ: проблема, которую я пытаюсь решить, заключается в следующем:

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


person axilmar    schedule 07.06.2010    source источник
comment
Чтобы добавить ответы sepp2k и Tal Pressman, вы также можете написать код Haskell, чтобы точно имитировать способ работы Java, избегая чистого функционального мира и используя монаду ST или IO. Использование STRefs для изменяемых указателей.   -  person yairchu    schedule 07.06.2010
comment
Интересный. Как это работает? компилятор рассматривает STRef как специальную конструкцию? и в чем разница между STRef и IORef? Я гуглил, но не нашел ничего понятного.   -  person axilmar    schedule 08.06.2010


Ответы (6)


Модификация дерева, которая может потребовать частых переходов вверх по пути к корню и обратно, кажется идеальной работой для варианта структуры данных Zipper со «шрамами» в терминологии оригинальной статьи Хьюэта; образцы кода из статьи также предполагают название «запоминание молнии». Конечно, с некоторой осторожностью можно использовать и обычную молнию, но расширенная версия может быть более удобной и/или эффективной в использовании.

Основная идея та же, что и в обычной застежке-молнии, которая уже позволяет перемещаться вверх и вниз по дереву чисто функциональным образом (без каких-либо явных обратных указателей), но операция «вверх», за которой следует операция «вверх». down» становится неоперативной, оставляя фокус на исходном узле (тогда как с обычной застежкой-молнией он переместится на крайний левый брат исходного узла).

Вот ссылка на статью: Жерар Юэ, Функциональная жемчужина: застежка-молния. Это всего лишь шесть страниц, но содержащиеся в них идеи будут очень полезны любому функциональному программисту.

person Michał Marczyk    schedule 07.06.2010
comment
статья о застежках-молниях в Haskell scienceblogs.com/goodmath/2010/01/ - person stonemetal; 07.06.2010
comment
Спасибо за документ, я читал о структуре молнии раньше. Я не понимаю, как использовать его в моей программе, хотя для поставленной задачи. Например, невозможно подняться и установить нового родителя для дочернего элемента с недопустимым свойством, установленным в значение true, потому что недействительность должна быть действительной для всех дочерних элементов. Если я использую застежку-молнию, мне придется изменить родительский объект для всех дочерних элементов, а поскольку мое дерево имеет 6 слоев в глубину, это изменение отразится на корневом объекте. - person axilmar; 07.06.2010
comment
Есть и еще одна проблема: я понимаю, как использовать данные функции (go_up, go_left и т. д.), и я также понимаю концепцию застежки-молнии (по сути, использовать локальные аргументы как деструктивно обновляемые переменные), но я не понимаю, как объединить его с остальной частью программы. Предположим, я использую функцию «go_up». Где сохранить результат? сохранить его в списке? и если да, то где хранить список? должен ли я сделать запись, содержащую все «текущие данные», такие как данные структуры молнии? Любые решения для этого очень ценятся. - person axilmar; 07.06.2010
comment
Я не уверен, что понимаю ваш первый комментарий. Если вы обновите родителя узла, то с этого момента у всех братьев и сестер узла также будет обновленный родитель. Если вам нужно пойти дальше - возможно, к прародительскому узлу - тогда любое обновление там будет видно как обновление прародительского узла для любых двоюродных узлов узла, на котором изначально был фокус. И так далее. В этом вся идея застежки-молнии: вы вносите изменения локально, затем в любой момент вы можете извлечь корневой узел из застежки-молнии, чтобы получить исходное дерево со всеми вашими выполненными обновлениями. - person Michał Marczyk; 07.06.2010
comment
Что касается сохранения результата, это тот же вопрос, что и при добавлении новой записи в список, где мне хранить полученный более длинный список? :-) Все функции управления застежкой-молнией (включая функции перемещения по застежке-молнии, а также функции вставки/обновления/удаления) воздействуют на так называемые местоположения застежки-молнии (сокращенно locs) и возвращают новые locs; затем вы можете взять эти новые локации и манипулировать ими дальше, или, если вы закончили обновление своего дерева, подняться по пути к корню и извлечь корневой узел (= все дерево) из молнии в его недавно обновленной форме. Надеюсь это поможет. - person Michał Marczyk; 07.06.2010
comment
Если вышеизложенное не ясно (и я не совсем доволен своей формулировкой - это только мой первый выстрел...), у меня есть только один вопрос, прежде чем я предприму еще одну попытку: после обновления узла объекта вы хотите сделать недействительным весь документ (скорее всего, установив какой-то флаг в корневом узле), верно? Или это что-то под рутом надо обновить? В любом случае это нормально с застежкой-молнией, просто прошу настроить любые примеры, которые я могу придумать, для вашего варианта использования... Кроме того, где вы хотите, чтобы фокус был после обновления и аннулирования? (Первоначально сфокусированный дочерний узел? Корень?) - person Michał Marczyk; 07.06.2010
comment
После обновления узла объекта я хочу аннулировать весь документ, установив некоторый флаг в корневом узле. Что касается фокуса, было бы неплохо иметь корневой узел. - person axilmar; 07.06.2010
comment
В этом случае начните с построения застежки-молнии для всего дерева, затем спуститесь к интересующему вас узлу, выполните обновление go_up пару раз, пока не окажетесь в корне (вы можете узнать, находитесь ли вы в root путем сопоставления с образцом -- только корневые локации имеют поле пути, установленное на Top), обновите флаг проверки в корне, а затем извлеките свое дерево. И вуаля, теперь у вас есть дерево точно такое же, как и исходное, но с измененным дочерним узлом и сброшенным флагом проверки в корне. - person Michał Marczyk; 07.06.2010
comment
Обратите внимание, что в статье Хьюэ описываются застежки-молнии на дереве, которые хранят данные только на листьях, однако адаптировать эту идею для деревьев, у которых есть декорированные внутренние узлы, несложно. Тем не менее, вам, вероятно, придется прочитать его раздел о застежках-молниях, уважающих арность, на разнородных деревьях. - person Michał Marczyk; 07.06.2010
comment
Но разве это не ужасно неэффективно? мой корневой узел, который мне нужно изменить на «недействительный», содержит много вещей. Каждый экземпляр имеет 30 полей. Если я реплицирую корневой экземпляр каждый раз, когда меняю его детали, моя программа будет ужасно неэффективной. Это также верно для дочерних узлов, которые содержат много информации. - person axilmar; 08.06.2010
comment
Застежки-молнии могут быть чрезвычайно эффективными, см., например. stackoverflow.com/questions/2531753/ (и, возможно, перейдите по ссылке на документ там). Но в конечном итоге вам придется выполнить некоторые измерения, чтобы выяснить, что эффективно в вашем конкретном случае. Может случиться так, что замена 30 полей означает просто замену 30 указателей... И тогда, если вы выполняете свои обновления пакетами, помечая корень как недействительный только один раз за пакет, проблем действительно не должно быть. Вам просто нужно пройти профиль, чтобы быть уверенным. - person Michał Marczyk; 08.06.2010

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

--               parent       children
data Tree = Node (Maybe Tree) [Tree]
root = Node Nothing [a,b] -- I can "forward reference" a and b because haskell is lazy
a = Node (Just root) []
b = Node (Just root) []

Вопрос в том, полезно ли это для вашего конкретного случая использования (часто это не так).

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

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

person sepp2k    schedule 07.06.2010
comment
Проблема, которую я пытаюсь решить, заключается в следующем: у меня есть приложение, которое редактирует документы. Документ представляет собой иерархию объектов. Когда свойства дочерних объектов изменяются, документ должен быть установлен в недопустимое состояние, чтобы пользователь знал, что документ необходимо проверить. - person axilmar; 07.06.2010

Вот некоторый код с застежкой-молнией, который демонстрирует простое изменение данных, на которые указывает курсор, а также «глобальное» свойство дерева. Мы строим дерево, перемещаем курсор к узлу, изначально содержащему 1, меняем его на 3, и остаемся с курсором, указывающим на этот узел в полностью обновленном дереве.

import Data.Maybe (fromJust)
import Data.Tree
import Data.Tree.Zipper

type NodeData = Either Bool Int
type TreePath a = [TreePos Full a -> TreePos Full a]

firstChild' = fromJust . firstChild
parent'     = fromJust . parent
prev'       = fromJust . prev
next'       = fromJust . next

-- Determine the path from the root of the tree to the cursor.
pathToMe :: TreePos Full NodeData -> TreePath NodeData
pathToMe t | isRoot t  = []
           | isFirst t = firstChild' : pathToMe (parent' t)
           | otherwise = next' : pathToMe (prev' t)

-- Mark a tree as invalid, but leave the cursor in the same place.
invalidate :: TreePos Full NodeData -> TreePos Full NodeData
invalidate t = foldr ($) (setLabel (Left False) (root t)) (pathToMe t)

-- Set a node's internal data.
setData :: Int -> TreePos Full NodeData -> TreePos Full NodeData
setData = (invalidate . ) . setLabel . Right

main = let tree1 = Node (Left True) [Node (Right 1) [], Node (Right 2) []]
           Just cursor = firstChild (fromTree tree1)
           tree2 = setData 3 cursor
       in do putStrLn (drawTree (fmap show tree1))
             putStrLn (drawTree (fmap show (toTree tree2)))
             putStrLn $ "Cursor at "++show (label tree2)

Выход:

Left True
|
+- Right 1
|
`- Right 2

Left False
|
+- Right 3
|
`- Right 2

Cursor at Right 3
person Anthony    schedule 07.06.2010
comment
Спасибо. Насколько я понимаю, приведенный выше код делает следующее: 1) создает дерево в файле main. 2) получает ребенка, получая курсор. 3) устанавливает данные, делает дерево недействительным и возвращает новое корневое дерево. Аннулирование происходит следующим образом: новый корневой узел создается из нового действительного флага и остальной части дерева. Я правильно понял вышеизложенное? - person axilmar; 07.06.2010
comment
Это правильно, с оговоркой, что setData не просто дает вам корень нового дерева, но новое дерево с курсором, установленным в том же месте, что и в исходном дереве (чтобы вы могли продолжать вносить последующие локальные изменения). - person Anthony; 07.06.2010
comment
Еще одно замечание: этот код можно сделать более эффективным, если не перестраивать дерево, если корень уже недействителен. - person Anthony; 07.06.2010
comment
Что делать, если корневой узел вызывает обратный вызов, когда он признан недействительным? в Java у меня может быть список объектов, которые будут уведомлены, когда объект станет недействительным. Как я могу сделать это с помощью Haskell? мне нужно будет перестраивать объект каждый раз, когда к объекту добавляется новый обратный вызов? как насчет тех ссылок на предыдущие корни, которые не обновляются? В Java обновление одного члена в корне отражается на всех объектах, ссылающихся на этот объект. - person axilmar; 08.06.2010
comment
Трудно дать абсолютно общие ответы на эти вопросы. Имейте в виду, что перестроение корня — очень дешевая операция, так как поддеревья, свисающие с него, не затрагиваются. Кроме того, использование застежки-молнии, как сделано здесь, означает, что не будет проблем с постоянными ссылками на старые корни, оставшиеся висящими. Таким образом, добавление нового обратного вызова требует перестроения корневого узла, но это не имеет большого значения. Если у вас есть другой конкретный проблемный случай, я бы посоветовал вам начать с чего-то вроде этого кода и опубликовать его как еще один вопрос, чтобы вы могли получить конкретные ответы. - person Anthony; 08.06.2010

У меня не так много опыта работы с Haskell, но, насколько мне известно, в чисто функциональных языках невозможно иметь круги в эталонном графе. Это означает, что:

  1. У вас не может быть двусторонних списков, детей в деревьях, указывающих на своих родителей и т. д.*
  2. Обычно недостаточно изменить только один узел. Любой измененный узел требует изменений в каждом узле, начиная с «корня» структур данных и заканчивая узлом, который вы хотите изменить.

Суть в том, что я бы не стал брать алгоритм Java (или любого другого императивного языка) и пытаться преобразовать его в Haskell. Вместо этого попробуйте найти более функциональный алгоритм (и, возможно, даже другую структуру данных) для решения проблемы.

ИЗМЕНИТЬ:

Из вашего разъяснения не совсем ясно, нужно ли вам аннулировать только прямого родителя объекта, который изменился, или всех его предков в иерархии, но на самом деле это не имеет большого значения. Поскольку аннулирование объекта в основном означает его изменение, а это невозможно, вам в основном нужно создать измененный дубликат этого объекта, а затем вы должны сделать так, чтобы его родительский объект указывал на него, поэтому вам также нужно создать новый объект для этого . Это продолжается до тех пор, пока вы не доберетесь до корня. Если у вас есть некоторая рекурсия для обхода дерева, чтобы «модифицировать» ваш объект, вы можете воссоздать путь от этого объекта к корню на выходе из рекурсии.

Надеюсь, это имело смысл. :с

* Как указано в комментариях jberryman и в других ответах, в Haskell можно создавать круговые ссылочные графы, используя ленивую оценку.

person Tal Pressman    schedule 07.06.2010
comment
На самом деле это не так. В Haskell лень позволяет нам определять структуру данных с точки зрения самой себя, даже очень сложными способами (гугл связывает себя узами брака). Так что двусвязный список не проблема. Но у вас возникают проблемы, когда вы пытаетесь изменить один из элементов списка :) - person jberryman; 07.06.2010

Изучите использование экземпляра Functor типа Maybe.

Например, ваша проблема может быть примерно такой: вы хотите вставить элемент в двоичное дерево, но только если его еще нет. Вы можете сделать это с помощью чего-то вроде:

data Tree a = Node a (Tree a) (Tree a)
            | Tip

maybeInsert :: a -> Tree a -> Maybe (Tree a)
maybeInsert a Tip = Just $ Node a Tip Tip
maybeInsert a (Node a' l r)
    | a == a' = Nothing
    | a < a'  = fmap (\l'-> Node a' l' r) (maybeInsert a l)
    | a > a'  = fmap (\r'-> Node a' l r') (maybeInsert a r)

Таким образом, функция вернет Nothing, если мы обнаружили, что элемент уже присутствует, или вернет Just новое дерево со вставленным элементом.

Надеюсь, это имеет отношение к тому, что вы пытаетесь сделать.

person jberryman    schedule 07.06.2010

Разве лень не может позаботиться о том, чтобы проверка не происходила слишком часто? Таким образом, вам не нужно хранить поле m_valid.

Например, если вы проверяете только при сохранении, то вы можете редактировать объекты по своему вкусу, без повторной проверки все время; только когда пользователь нажимает кнопку «Сохранить», вычисляется значение validateDoc. Поскольку я не знаю наверняка, что означает ваше понятие валидности и для чего оно вам нужно, я могу быть совершенно прав.

Непроверенный и неполный код:

data Document = Document { subDocs :: [SubDoc] }
data SubDoc = SubDoc { content :: String }

addSubDoc :: SubDoc -> (Document -> Document)
addSubDoc = error "not yet implemented: addSubDoc"

modifySubDoc :: Int -> (SubDoc -> SubDoc) -> (Document -> Document)
modifySubDoc = error "not yet implemented: modifySubDoc"


validateDoc :: Document -> Bool
validateDoc = all validateSubDoc . subDocs

validateSubDoc :: SubDoc -> Bool
validateSubDoc = not . null . contents

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

Кстати, я думаю, вы забыли a.addChild(b); в main.

person yatima2975    schedule 08.06.2010
comment
Действительный документ означает, что он прошел определенный тест. Идея здесь в том, что пользователь редактирует документ, а затем проверяет его. - person axilmar; 08.06.2010