manejar - Haskell: profundidad para cada nodo en árbol binario utilizando la mónada Reader
ejercicio arboles binarios (2)
No es necesario entrar y salir del Reader
la manera que lo hace aquí usando runReader
; en cambio, puedes reescribirlo como
renumberR :: Tree a -> Reader Int (Tree Int)
renumberR (Node _ l r) = do
x <- ask
l'' <- local (+1) (renumberR l)
r'' <- local (+1) (renumberR r)
return (Node x l'' r'')
renumberR Empty = return Empty
Sin embargo, puedes escribirlo aún mejor usando solo la interfaz de Reader
:
renumberR (Node _ l r) =
Node <$> ask <*> local (+1) (renumberR l) <*> local (+1) (renumberR r)
renumberR Empty = pure Empty
Tenga en cuenta que he cambiado el nombre de su función para renumberR
para enfatizar el hecho de que se ejecuta en Reader
, pero no necesariamente utilizando su interfaz monádica.
Escribí el siguiente código. Está funcionando y usando la mónada Reader
.
¿Podría darme algunas pistas sobre el estilo del código en Haskell? Principalmente, me refiero a mónadas: soy novato.
import Control.Monad.Reader
data Tree a = Node a (Tree a) (Tree a)
| Empty
renumberM :: Tree a -> Reader Int (Tree Int)
renumberM (Node _ l r) = ask >>= (/x ->
return (Node x (runReader (local (+1) (renumberM l)) x)
(runReader (local (+1) (renumberM r)) x)))
renumberM Empty = return Empty
renumber'''' :: Tree a -> Tree Int
renumber'''' t = runReader (renumberM t) 0
Quiero mostrarte que tu idea es una instancia de un concepto más general: compresión . Aquí hay una versión de su programa que emplea un estilo más simple y más funcional.
Funcionantes Aplicativos
Aquí está la definición de Applicative
:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
Se podría decir que un tipo fx
es una estructura f
contiene algunos valores x
. La función <*>
toma una estructura de funciones ( f (a -> b)
) y la aplica a una estructura de argumentos ( fa
) para producir una estructura de resultados ( fb
).
Aplicadores Zippy
Una forma de hacer de Tree
un funtor aplicativo es haciendo <*>
atravesar los dos árboles en el paso de bloqueo, comprimiéndolos juntos como lo hace zip
con las listas. Cada vez que encuentre un Node
en el árbol de funciones y un Node
en el árbol de argumentos, puede sacar la función y aplicarla al argumento. Tienes que dejar de atravesar cuando llegues al final de cualquiera de los árboles.
instance Applicative Tree where
pure x = let t = Node x t t in t
Empty <*> _ = Empty
_ <*> Empty = Empty
(Node f lf rf) <*> (Node x lx rx) = Node (f x) (lf <*> lx) (rf <*> rx)
instance Functor Tree where
fmap f x = pure f <*> x -- as usual
pure x
genera un árbol infinito de x
s. Esto funciona bien porque Haskell es un lenguaje perezoso.
+-----x-----+
| |
+--x--+ +--x--+
| | | |
+-x-+ +-x-+ +-x-+ +-x-+
| | | | | | | |
etc
Entonces la forma del árbol t <*> pure x
es la misma que la forma de t
: solo dejas de atravesar cuando encuentras un Empty
, y no hay ninguno en pure x
. (Lo mismo se aplica a pure x <*> t
)
Esta es una forma común de hacer que una estructura de datos sea una instancia de Applicative
. Por ejemplo, la biblioteca estándar incluye ZipList
, cuya instancia Applicative
es muy similar a la de nuestro árbol:
newtype ZipList a = ZipList { getZipList :: [a] }
instance Applicative ZipList where
pure x = ZipList (repeat x)
ZipList fs <*> ZipList xs = ZipList (zipWith ($) fs xs)
Una vez más, pure
genera un ZipList
infinito y <*>
consume sus argumentos en lock-step.
El zippy prototipo Aplicable, si lo desea, es el "lector" Aplicativo (->) r
, que combina funciones al aplicarlas todas a un argumento fijo y recoger los resultados. Por lo tanto, todos los funtores Representable
admiten (al menos) una instancia Applicable rápida.
Utilizando alguna maquinaria Aplicable , podemos generalizar el archivo zip
del Prelude a cualquier funcionador aplicativo (aunque solo se comportará exactamente como zip
cuando el Applicative
sea de naturaleza zippy; por ejemplo, con la instancia Applicative
regular para []
zipA
le dará el Cartesiano producto de sus argumentos).
zipA :: Applicative f => f a -> f b -> f (a, b)
zipA = liftA2 (,)
Etiquetado como Zipping
El plan es comprimir el árbol de entrada junto con un árbol infinito que contenga la profundidad de cada nivel. El resultado será un árbol con la misma forma que el árbol de entrada (porque el árbol de profundidades es infinito), pero cada nodo se etiquetará con su profundidad.
depths :: Tree Integer
depths = go 0
where go n = let t = go (n+1) in Node n t t
Así es como se ve la depths
:
+-----0-----+
| |
+--1--+ +--1--+
| | | |
+-2-+ +-2-+ +-2-+ +-2-+
| | | | | | | |
etc
Ahora que hemos configurado las estructuras que necesitamos, etiquetar un árbol es fácil.
labelDepths :: Tree a -> Tree (Integer, a)
labelDepths = zipA depths
Volver a etiquetar un árbol tirando las etiquetas originales, como lo especificó originalmente, también es fácil .
relabelDepths :: Tree a -> Tree Integer
relabelDepths t = t *> depths
Una prueba rápida:
ghci> let myT = Node ''x'' (Node ''y'' (Node ''z'' Empty Empty) (Node ''a'' Empty Empty)) (Node ''b'' Empty Empty)
ghci> labelDepths myT
Node (0,''x'') (Node (1,''y'') (Node (2,''z'') Empty Empty) (Node (2,''a'') Empty Empty)) (Node (1,''b'') Empty Empty)
+--''x''-+ +--(0,''x'')-+
| | labelDepths | |
+-''y''-+ ''b'' ~~> +-(1,''y'')-+ (1,''b'')
| | | |
''z'' ''a'' (2,''z'') (2,''a'')
Puede idear diferentes esquemas de etiquetado variando el árbol que recorre. Aquí hay una que te dice el camino que tomaste para llegar a un nodo:
data Step = L | R
type Path = [Step]
paths :: Tree Path
paths = go []
where go path = Node path (go (path ++ [L])) (go (path ++ [R]))
+--------[ ]--------+
| |
+---[L]---+ +---[R]---+
| | | |
+-[L,L]-+ +-[L,R]-+ +-[R,L]-+ +-[R,R]-+
| | | | | | | |
etc
(El anidamiento ineficiente de llamadas a ++
arriba puede mitigarse usando listas de diferencias ).
labelPath :: Tree a -> Tree (Path, a)
labelPath = zipA paths
A medida que continúe aprendiendo Haskell, obtendrá una mejor detección cuando un programa sea un ejemplo de un concepto más profundo. La configuración de estructuras generales, como hice con la instancia Applicative
anterior, rápidamente paga dividendos en la reutilización del código.