2010-06-07 11 views
7

Tengo el siguiente problema: Tengo un árbol de objetos de diferentes clases donde una acción en la clase secundaria invalida el elemento primario. En lenguajes imperativos, es trivial de hacer. Por ejemplo, en Java:¿Cómo codigo un árbol de objetos en Haskell con punteros a padres e hijos?

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 
    } 
} 

¿Cómo hago lo anterior en Haskell? No puedo entender esto, ya que una vez que construyo un objeto en Haskell, no puede ser cambiado.

Me agradecería mucho si se publica el código Haskell correspondiente.

EDIT: el problema que estoy tratando de resolver es el siguiente:

He una aplicación que edita documentos. Un documento es una jerarquía de objetos. Cuando se modifican las propiedades de los objetos secundarios, el documento debe establecerse en un estado no válido, para que el usuario sepa que el documento debe validarse.

+0

Para agregar las respuestas de sepp2k y Tal Pressman, también puede escribir el código Haskell para imitar exactamente la forma de hacer las cosas de Java, escapando del mundo funcional puro y utilizando la mónada 'ST' o' IO'. Usando 'STRef's para los punteros" cambiables ". – yairchu

+0

Interesante. ¿Como funciona? ¿el compilador trata a STRef como una construcción especial? y ¿cuáles son las diferencias entre STRef e IORef? Lo busqué en Google, pero no se me ocurrió algo fácil de comprender. – axilmar

Respuesta

2

No tengo mucha experiencia con Haskell, pero hasta donde sé, no es posible tener círculos en el gráfico de referencia en lenguajes funcionales puros. Eso significa que:

  1. Usted no puede tener un 2 vías listas, los niños en los árboles que apuntan a sus padres, etc. *
  2. En general no es suficiente para cambiar un solo nodo. Cualquier nodo que se modifique requiere cambios en cada nodo, comenzando desde la "raíz" de las estructuras de datos hasta el nodo que desea cambiar.

La conclusión es que no trataría de tomar un algoritmo Java (o cualquier otro lenguaje imperativo) e intentar convertirlo a Haskell. En su lugar, intente encontrar un algoritmo más funcional (y tal vez incluso una estructura de datos diferente) para resolver el problema.

EDIT:

Desde su aclaración no es del todo claro si necesita o no para invalidar sólo el padre directa del objeto que ha cambiado o todas sus antepasados ​​en la jerarquía, pero que en realidad no importa tanto. Ya que invalidar un objeto básicamente significa cambiarlo y eso no es posible, básicamente tiene que crear un duplicado modificado de ese objeto, y luego tiene que hacer que su padre lo señale, por lo que debe crear un nuevo objeto para eso también. . Esto continúa hasta llegar a la raíz. Si tiene alguna recursividad para atravesar el árbol para "modificar" su objeto, puede volver a crear la ruta desde ese objeto hasta la raíz al salir de la recursión.

Espero que tenga sentido. : s

* Como se señala en los comentarios de jberryman y en otras respuestas, es posible crear gráficos de referencia circulares en Haskell utilizando evaluación diferida.

+4

En realidad eso no es verdad. En haskell la pereza nos permite definir una estructura de datos en términos de sí misma, incluso de formas muy complejas (google "atar el nudo"). Entonces, una lista doblemente enlazada no es un problema. Pero te metes en problemas cuando tratas de modificar uno de los elementos de la lista :) – jberryman

+0

Hmm, es bueno saberlo. Editaré mi respuesta para solucionarlo. –

3

Para responder la pregunta en su título: Sí, puede crear nodos que tengan enlaces a sus padres y sus hijos. Ejemplo:

--    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) [] 

La pregunta es si esto es útil para su caso de uso en particular (muchas veces no lo es).

Ahora la pregunta en su cuerpo: tiene razón, no puede cambiar un valor después de haber sido creado.Entonces, una vez que tenga un árbol válido, siempre tendrá un árbol válido siempre que la variable que hace referencia a ese árbol esté dentro del alcance.

Realmente no describiste el problema que estás tratando de resolver, así que no puedo decirte cómo modelar funcionalmente lo que estás tratando de hacer, pero estoy seguro de que hay una manera de no mutar el árbol .

+0

El problema que intento resolver es el siguiente: Tengo una aplicación que edita documentos. Un documento es una jerarquía de objetos. Cuando se modifican las propiedades de los objetos secundarios, el documento debe establecerse en un estado no válido, para que el usuario sepa que el documento debe validarse. – axilmar

0

Examine el uso de la instancia Functor del tipo Maybe.

Por ejemplo, tal vez su problema es algo como esto: desea insertar un elemento en un árbol binario, pero solo si no está presente. Se podría hacer eso con algo como:

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) 

Así que la función devolverá Nothing si encontramos el elemento que ya está presente, o volver Just el nuevo árbol con el elemento insertado.

Esperemos que sea relevante para lo que sea que estés tratando de hacer.

7

La modificación de un árbol que podría requerir excursiones frecuentes hasta la raíz y la parte posterior parece ser el trabajo perfecto para una variante de la estructura de datos Zipper con "cicatrices", en la terminología del documento original de Huet; las muestras de código del documento también sugieren un nombre de "memorización de cremallera". Por supuesto, con cierto cuidado, también se podría usar una cremallera normal, pero la versión aumentada podría ser más conveniente y/o eficiente de usar.

La idea básica es la misma que la de una cremallera normal, que ya permite subir y bajar un árbol de una manera puramente funcional (sin punteros atrás explícitos), pero se siguió una operación de "subir" mediante una operación "descender" se convierte en no operativa, dejando el foco en el nodo original (mientras que con la cremallera normal lo movería al hermano más a la izquierda del nodo original).

Aquí hay un enlace al artículo: Gérard Huet, Functional Pearl: The Zipper. Son solo seis páginas, pero las ideas contenidas en ellas son de gran utilidad para cualquier programador funcional.

+0

un artículo sobre cremalleras en haskell http://scienceblogs.com/goodmath/2010/01/zippers_making_functional_upda.php – stonemetal

+0

Gracias por el documento, he leído sobre la estructura de cremallera antes. Aunque no entiendo cómo usarlo en mi programa para la tarea en cuestión. Por ejemplo, no es posible subir y establecer un nuevo elemento primario para el elemento secundario con la propiedad "no válida" establecida en verdadero, porque la invalidación debe ser válida para todos los elementos secundarios. Si utilizo la estructura de cremallera, tendría que cambiar el objeto principal para todos los elementos secundarios, y como mi árbol tiene 6 capas de profundidad, este cambio ondulará al objeto raíz. – axilmar

+0

También hay otro problema: entiendo cómo usar las funciones dadas ("go_up", "go_left", etc.), y también entiendo el concepto de la cremallera (esencialmente uso los argumentos locales como variables actualizadas destructivamente), pero no No entiendo cómo combinarlo con el resto del programa. Supongamos que uso la función 'go_up'. ¿Dónde guardo el resultado? ¿Lo guardo en una lista? y si es así, ¿dónde guardo la lista? ¿Debería hacer un registro que contenga todos los 'datos actuales', como los datos de la estructura de la cremallera? Cualquier solución a esto es muy apreciada. – axilmar

3

Aquí hay un código de cremallera que muestra la fácil modificación de los datos apuntados por un cursor, así como una propiedad "global" del árbol. Construimos un árbol, movemos el cursor al nodo que inicialmente contiene un 1, lo cambiamos a un 3 y quedamos con un cursor apuntando a ese nodo en un árbol totalmente actualizado.

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) 

Salida:

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

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

Cursor at Right 3 
+0

Gracias. Por lo que entiendo, el código anterior hace lo siguiente: 1) crea un árbol en main. 2) obtiene un niño al obtener un cursor. 3) establece los datos, invalida el árbol y devuelve un nuevo árbol raíz. La invalidación ocurre así: se construye un nuevo nodo raíz a partir del nuevo indicador válido y el resto del árbol. ¿He entendido lo anterior correctamente? – axilmar

+0

Eso es correcto, con la advertencia de que setData no solo le proporciona la raíz del nuevo árbol, sino un árbol nuevo con el cursor establecido en la misma ubicación que en el árbol original (para que pueda continuar realizando los cambios locales posteriores) . – Anthony

+0

Otra nota es que este código podría ser más eficiente al no reconstruir el árbol cuando la raíz ya no es válida. – Anthony

0

No se pudo pereza tener cuidado de asegurarse de que la validación no sucede muy a menudo? De esta forma, no necesita almacenar el campo m_valid.

Por ejemplo, si solo valida al guardar, puede editar los objetos a su gusto, sin revalidar todo el tiempo; solo cuando el usuario presiona el botón "Guardar" se calcula el valor de validateDoc. Como no estoy seguro de cuál es su noción de los medios válidos y para qué los necesita, podría ser totalmente de la marca.

no probados & código incompleto:

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 

Asumo la validez general del documento depende sólo de los subdocumentos (simulado aquí, haciendo que éstos contienen una cadena no vacía).

Por cierto, creo que se olvidó de a.addChild(b); en main.

+0

El documento válido significa que pasa una determinada prueba. La idea aquí es que el usuario edite el documento y luego lo valida. – axilmar

Cuestiones relacionadas