haskell zipper comonad

haskell - Comonadicamente encontrando todas las formas de enfocarse en una cuadrícula



zipper (3)

Esta pregunta ya tiene una respuesta aquí:

{-# LANGUAGE DeriveFoldable #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveTraversable #-} import Control.Comonad import Data.Functor.Reverse import Data.List (unfoldr)

Primero algún contexto (ja ja). Tengo una zipper sobre las listas no vacías.

data LZipper a = LZipper (Reverse [] a) a [a] deriving (Eq, Ord, Show, Read, Functor, Foldable, Traversable) mkZipper :: a -> [a] -> LZipper a mkZipper = LZipper (Reverse [])

Puede avanzar en cualquier dirección a lo largo de la cremallera, pero podría caerse del extremo.

fwd, bwd :: LZipper a -> Maybe (LZipper a) fwd (LZipper _ _ []) = Nothing fwd (LZipper (Reverse xs) e (y:ys)) = Just $ LZipper (Reverse (e:xs)) y ys bwd (LZipper (Reverse []) _ _) = Nothing bwd (LZipper (Reverse (x:xs)) e ys) = Just $ LZipper (Reverse xs) x (e:ys)

Duplicar una cremallera le muestra todas las formas en que podría mirarlo, con el enfoque en la forma en que lo está viendo actualmente.

instance Comonad LZipper where extract (LZipper _ x _) = x duplicate z = LZipper (Reverse $ unfoldr (step bwd) z) z (unfoldr (step fwd) z) where step move = fmap (/y -> (y, y)) . move

Por ejemplo:

ghci> duplicate (mkZipper ''a'' "bc") LZipper (Reverse []) (LZipper (Reverse "") ''a'' "bc") [LZipper (Reverse "a") ''b'' "c",LZipper (Reverse "ba") ''c'' ""] -- Abc -> *Abc* aBc abC ghci> fmap duplicate (fwd $ mkZipper ''a'' "bc") Just (LZipper (Reverse [LZipper (Reverse "") ''a'' "bc"]) (LZipper (Reverse "a") ''b'' "c") [LZipper (Reverse "ba") ''c'' ""]) -- aBc -> Abc *aBc* abC

(Estoy usando mayúsculas y asteriscos para indicar el punto focal de la cremallera).

Estoy tratando de trabajar con grillas de dos dimensiones con un enfoque, representado como una cremallera de cremalleras. Cada cremallera interior es una fila de la rejilla. Mi objetivo final es encontrar caminos a través de una cuadrícula saltando de vecino a vecino.

Moverse a través de la cuadrícula mantiene el invariante de que todas las filas están enfocadas en el mismo índice. Esto hace que sea fácil concentrarse en cualquiera de sus vecinos.

type Grid a = LZipper (LZipper a) up, down, left, right :: Grid a -> Maybe (Grid a) up = bwd down = fwd left = traverse bwd right = traverse fwd extractGrid :: Grid a -> a extractGrid = extract . extract mkGrid :: (a, [a]) -> [(a, [a])] -> Grid a mkGrid (x, xs) xss = mkZipper (mkZipper x xs) $ map (uncurry mkZipper) xss

Ejemplos:

ghci> let myGrid = mkGrid (''a'', "bc") [(''d'', "ef"), (''g'', "hi")] ghci> myGrid LZipper (Reverse []) (LZipper (Reverse "") ''a'' "bc") [LZipper (Reverse "") ''d'' "ef",LZipper (Reverse "") ''g'' "hi"] -- +-------+ -- | A b c | -- | d e f | -- | g h i | -- +-------+ ghci> return myGrid >>= right >>= down Just (LZipper (Reverse [LZipper (Reverse "a") ''b'' "c"]) (LZipper (Reverse "d") ''e'' "f") [LZipper (Reverse "g") ''h'' "i"]) -- +-------+ -- | a b c | -- | d E f | -- | g h i | -- +-------+

Lo que quiero es el equivalente del LZipper de LZipper para cuadrículas: una función que toma una cuadrícula y produce una cuadrícula de todas las formas en que podría mirar la cuadrícula, con el enfoque en la forma actual en que lo está viendo.

duplicateGrid :: Grid a -> Grid (Grid a)

Lo que estoy esperando:

duplicateGrid myGrid +-------------------------------+ | ********* +-------+ +-------+ | | * A b c * | a B c | | a b C | | | * d e f * | d e f | | d e f | | | * g h i * | g h i | | g h i | | | ********* +-------+ +-------+ | | +-------+ +-------+ +-------+ | | | a b c | | a b c | | a b c | | | | D e f | | d E f | | d e F | | | | g h i | | g h i | | g h i | | | +-------+ +-------+ +-------+ | | +-------+ +-------+ +-------+ | | | a b c | | a b c | | a b c | | | | d e f | | d e f | | d e f | | | | G h i | | g H i | | g h I | | | +-------+ +-------+ +-------+ | +-------------------------------+

Intenté duplicateGrid = duplicate . duplicate duplicateGrid = duplicate . duplicate Esto tiene el tipo correcto, pero (asumiendo que interpreté la salida del show correctamente, lo cual probablemente no lo hice) solo me da cuadrículas enfocadas en algún lugar de la primera columna:

(duplicate . duplicate) myGrid +-------------------------------+ | ********* +-------+ +-------+ | | * A b c * | a b c | | a b c | | | * d e f * | D e f | | d e f | | | * g h i * | g h i | | G h i | | | ********* +-------+ +-------+ | | +-------+ +-------+ +-------+ | | | A b c | | a b c | | a b c | | | | d e f | | D e f | | d e f | | | | g h i | | g h i | | G h i | | | +-------+ +-------+ +-------+ | | +-------+ +-------+ +-------+ | | | A b c | | a b c | | a b c | | | | d e f | | D e f | | d e f | | | | g h i | | g h i | | G h i | | | +-------+ +-------+ +-------+ | +-------------------------------+

También probé duplicateGrid = duplicate . fmap duplicate duplicateGrid = duplicate . fmap duplicate . Suponiendo una vez más que soy capaz de interpretar la salida del show , esto me dio algo que contenía las cuadrículas incorrectas y tenía los enfoques de las filas desalineados, de modo que al moverte hacia abajo también te moverías:

(duplicate . fmap duplicate) myGrid +-------------------------------+ | ********* +-------+ +-------+ | | * A b c * | D e f | | G h i | | | * a B c * | d E f | | g H i | | | * a b C * | d e F | | g h I | | | ********* +-------+ +-------+ | | +-------+ ********* +-------+ | | | A b c | * D e f * | G h i | | | | a B c | * d E f * | g H i | | | | a b C | * d e F * | g h I | | | +-------+ ********* +-------+ | | +-------+ +-------+ ********* | | | A b c | | D e f | * G h i * | | | a B c | | d E f | * g H i * | | | a b C | | d e F | * g h I * | | +-------+ +-------+ ********* | +-------------------------------+

Esto parece que sería una pregunta fácil para los que saben, pero está haciendo que mi cabeza gire. Supongo que podría manejar manualmente una función que llama up , down , left y right , pero siento que la maquinaria común debería poder hacerlo por mí. ¿Cuál es la implementación correcta de duplicateGrid ?


Así que hay un comonad estrechamente relacionado que puede ayudarte a guiarte. Tenemos:

newtype MC m a = MC { unMC :: m -> a } instance Monoid m => Comonad (MC m) where extract (MC f) = f mempty duplicate (MC f) = MC (/x -> MC (/y -> f (x <> y))) instance Functor (MC m) where fmap f (MC g) = MC (f . g)

Entonces, una matriz bidireccionalmente infinita sería MC (Sum Integer) a y una cuadrícula bidireccionalmente infinita sería MC (Sum Integer, Sum Integer) a . Y, por supuesto, MC m (MC na) es isomorfo a MC (m,n) a través del curry.

En cualquier caso, su función de cuadrícula duplicada deseada sería análoga (ignorando las envolturas de tipo nuevo y el curry) a:

duplicateGrid g x y dx dy = g (x + dx) (y + dy)

duplicate para la matriz 1D se ve así:

duplicate f x y = f (x+y)

Así que duplicate . duplicate duplicate . duplicate es:

(duplicate . duplicate) f x y z = duplicate (duplicate f) x y z = duplicate f (x+y) z = f (x + y + z)

No es lo que se quiere. ¿Cómo se fmap duplicate ?

fmap duplicate f x y z = f x (y + z)

Está claro que hacer duplicate nuevo nos dará lo mismo que duplicate . duplicate duplicate . duplicate (que debería ser una ley de comuna). Sin embargo, esto es un poco más prometedor. Si fmap dos fmap s ...

fmap (fmap duplicate) f x y z w = fmap duplicate (f x) y z w = f x y (z + w)

Ahora si hiciéramos el duplicate obtendríamos

(duplicate . fmap (fmap duplicate)) f x y z w = f (x+y) (z+w)

Pero esto todavía está mal. Cambiando nombres de variables, su f (x+y) (dx + dy) . Entonces, necesitamos algo para intercambiar las dos variables internas ... El nombre de la teoría de categorías para lo que queremos es una ley distributiva. El nombre de Haskell es Traversable . ¿Cómo sequenceA ve la sequenceA para las funciones (las funciones de un funtor de Applicative y, de hecho, una Monad , la mónada del Reader )? El tipo lo dice todo.

sequenceA :: (a -> b -> c) -> (b -> a -> c) sequenceA f x y = f y x

Así que finalmente:

fmap sequenceA g x y z = g x z y (duplicate . fmap (fmap duplicate) . fmap sequenceA) g x y dx dy = (duplicate . fmap (fmap duplicate)) g x dx y dy = g (x + dx) (y + dy)

No he probado el código análogo, así que no sé si funciona, pero las matemáticas dicen que debería.


El problema fundamental con el que se encuentra es que las cremalleras no admiten de manera nativa estructuras 2-d . La respuesta es excelente (la otra respuesta es básicamente su definición de Grid ) y lo alentaría a que la leyera, pero lo esencial es que las cremalleras identifican los elementos con los caminos para llegar allí y en un espacio bidimensional, tal identificación es Es problemático porque hay muchos caminos para llegar a un punto.

Por lo tanto, notará que mientras sus funciones de up y down para Grid s se definieron completamente en términos de Cremalleras, tuvo que usar la maquinaria de Traversable para definir la left y la right . Esto también significa que la left y la right no disfrutan de las mismas propiedades de rendimiento como up y down ya que estás "en contra del grano", por así decirlo.

Dado que su instancia de Comonad se definió solo mediante las funciones de cremallera, solo puede duplicate en la dirección definida por su cremallera, es decir, fwd y bwd (y por extensión up y down ).

Edit : Después de pensar un poco más, creo que su enfoque será fundamentalmente problemático. He conservado mi texto original a continuación, pero hay un problema más evidente.

Si intentas atravesar las cremalleras como si fueran como cualquier otra estructura 2-d, seguirás obteniendo Nothing con tu duplicate . Observemos qué sucede si realmente intenta utilizar las funciones up, down, left, right en el duplicate (mkZipper ''a'' "bc") aparentemente no problemático duplicate (mkZipper ''a'' "bc") .

*Main> let allRows = duplicate $ mkZipper ''a'' "bc" *Main> down allRows -- This is fine since we''re following the zipper normally Just (LZipper (Backwards [LZipper (Backwards "") ''a'' "bc"]) (LZipper (Backwards "a") ''b'' "c") [LZipper (Backwards "ba") ''c'' ""]) *Main> right allRows Nothing -- That''s bad... *Main> down allRows >>= right Nothing -- Still nothing

Moverse hacia la right y hacia la left requiere (como usted nota debidamente con su mención del invariante) que cada una de sus sub-cremalleras es homogénea en su estructura, de lo contrario, la traverse fallará prematuramente. Esto significa que si realmente quieres usar la left y la right , la única forma en que esto va a jugar bien con el duplicate es si usas el duplicate más uniforme posible.

duplicate z @ (LZipper left focus right) = LZipper (fmap (const z) left) z (fmap (const z) right)

La alternativa es usar solo las funciones que vienen con la cremallera. Eso significa solo usar fwd y bwd , y luego extract el foco y continuar usando fwd y bwd para obtener lo mismo que a left y right . Por supuesto, eso significa renunciar a la habilidad de decir "justo en el momento" y "abajo que en la derecha", pero como ya hemos visto, las cremalleras no juegan bien con múltiples caminos.

Ahora revisemos sus intuiciones sobre la mejor manera de interpretar lo que estaba sucediendo con el duplicate . duplicate $ myGrid duplicate . duplicate $ myGrid . Una plaza bonita no es realmente la mejor forma de pensar sobre lo que está pasando (y verá por qué, si se limita a extract y fwd y bwd ).

*Main> let allRows = duplicate . duplicate $ myGrid *Main> fwd $ extract allRows -- Makes sense Just ... -- This *should* be the bottom-left of the grid *Main> let bottomLeft = extract <$> fwd allRows >>= fwd *Main> bottomLeft >>= fwd Nothing -- Nope! *Main> bottomLeft >>= bwd Just ... -- Wait a minute...

En realidad tenemos una estructura irregular.

+---------------------------------------------------+ | ********* +-------+ +-------+ | | * A b c * | a b c | | a b c | | | * d e f * | D e f | | d e f | | | * g h i * | g h i | | G h i | | | ********* +-------+ +-------+ | | +-------+ +-------+ +-------+ | | | A b c | | a b c | | a b c | | | | d e f | | D e f | | d e f | | | | g h i | | g h i | | G h i | | | +-------+ +-------+ +-------+ | | +-------+ +-------+ +-------+ | | | A b c | | a b c | | a b c | | | | d e f | | D e f | | d e f | | | | g h i | | g h i | | G h i | | | +-------+ +-------+ +-------+ | +---------------------------------------------------+

Los cuadrados dentro de esta estructura irregular tampoco son en realidad cuadrados, también serán irregulares. De manera equivalente, se podría pensar que fwd va en diagonal. O simplemente suelte las cremalleras para estructuras en 2-d en total.

En mi experiencia, las cremalleras realmente funcionan mejor cuando se combinan con cosas parecidas a árboles. No me sorprendería si un experto en Haskell pudiera encontrar una forma de usar cremalleras y toda la actualización / acceso a la bondad que vienen con ellos para cosas como gráficos cíclicos o simplemente DAG antiguos, pero no puedo pensar en ninguna. fuera de la parte superior de mi cabeza magra :).

Así que, moraleja de la historia, las cremalleras son un dolor de cabeza para las estructuras 2-d. (Pensamiento ocioso: tal vez las lentes podrían ser interesantes?)

Para los curiosos, mi enfoque a continuación solo funciona si se tiene en cuenta la irregularidad de la estructura con la que estamos tratando; eso es fwd dos veces y luego extraerlo te dará el equivalente a lo que OP quiere en la esquina inferior derecha de su cuadrícula, no en el lado inferior izquierdo.

Original :

Entonces, lo que necesita es alguna forma de cambiar entre su duplicate basado en la cremallera y su duplicado basado en el desplazamiento. La forma más fácil es tomar su función duplicate que ya ha escrito y simplemente agregar una traverse en el medio.

duplicateT :: Traversable t => t (LZipper a) -> LZipper (t (LZipper a)) duplicateT z = LZipper (Backwards $ unfoldr (step bwd) z) z (unfoldr (step fwd) z) -- Everything''s the exact same except for that extra traverse where step move = fmap (/y -> (y, y)) . (traverse move)

Ahora que tenemos un duplicateT más general, podemos deshacernos de una duplicación de código desagradable redefiniendo el duplicate en su instancia de Comonad para que sea:

-- requires import Data.Functor.Identity duplicate = fmap runIdentity (duplicate'' (Identity z))

Entonces lo siguiente te da lo que quieres

duplicateGrid = duplicate . duplicateT

O si desea cambiar el orden de las columnas y filas, puede hacer lo contrario.

Nota: sería aún más agradable si Haskell le permitiera definir de forma nativa las restricciones de tipo en las clases de tipos para que pueda tener diferentes instancias de Comonad (quizás todas ellas mediadas por newtype ) para su LZipper que cambie la dirección de su duplicate . El problema es que querrías algo como la instance Comonad LZipper (LZipper a) where ... o el nuevo tipo equivalente que simplemente no puedes escribir en Haskell. Posiblemente podría hacer algo como this con familias de tipos, pero sospecho que probablemente sea excesivo para este caso en particular.

Edición : De hecho, ni siquiera necesita duplicateT LZipper si da la instancia de Applicative adecuada para LZipper .

instance Applicative LZipper where pure x = LZipper (Backwards (repeat x)) x (repeat x) (LZipper leftF f rightF) <*> (LZipper left x right) = LZipper newLeft (f x) newRight where newLeft = (Backwards (zipWith ($) (forwards leftF) (forwards left))) newRight = (zipWith ($) rightF right)

Ahora simplemente toma el duplicate original que tenías antes y usa la traverse .

duplicateGrid = duplicate . (traverse duplicate)


Es un problema que estamos tratando de componer Grid consigo mismo, porque esta configuración nos brinda demasiadas formas incorrectas de implementar un duplicate con el tipo correcto. Es útil considerar el caso general en el que las comillas compuestas no son necesariamente las mismas.

Supongamos que tenemos comillas f y g . El tipo de duplicate convierte en:

duplicate :: f (g a) -> f (g (f (g a)))

Podemos obtener lo siguiente únicamente utilizando las instancias de Comonad :

duplicate . fmap duplicate :: f (g a) -> f (f (g (g a)))

De esto se hace evidente que necesitamos intercambiar f y g en el medio.

Hay una clase de tipo llamada Distributive que tiene el método que queremos.

class Functor g => Distributive g where distribute :: Functor f => f (g a) -> g (f a)

En particular, necesitamos implementar Distributive g , y luego duplicate para el comonad compuesto se puede implementar como:

duplicate = fmap distribute . duplicate . fmap duplicate

Sin embargo, la documentación en Distributive dice que los valores de g deben tener exactamente la misma forma, por lo que podemos comprimir un número arbitrario de copias sin pérdida de información.

Para ilustrar esto, si Vec na es un vector de tamaño n , entonces distribute :: [Vec na] -> Vec n [a] es solo una transposición de matriz. Es necesario fijar el tamaño hacia abajo del vector interior de antemano, porque la transposición en una matriz "irregular" debe eliminar algunos elementos, y eso no es un comportamiento legal. Las corrientes infinitas y las cremalleras también se distribuyen bien, ya que también tienen un solo tamaño posible.

Zipper no es un Distributive legal porque Zipper contiene valores con contextos de diferentes tamaños. Aún así, podemos implementar una distribución inadecuada que supone tamaños de contexto uniformes.

A continuación, implementaré el duplicate de Grid en términos de distribución inadecuada para las listas subyacentes.

Alternativamente, uno podría simplemente enrollar sus mangas e implementar una función de transposición en Zipper (Zipper a) directamente. De hecho, hice esto, pero me dio un dolor de cabeza y estoy lejos de estar seguro de que es correcto. Es mejor hacer los tipos tan generales como sea posible, para reducir el espacio de posibles implementaciones, por lo que hay menos espacio para errores.

Voy a omitir Reverse para reducir el ruido sintáctico; Espero que me disculpes.

{-# language DeriveFunctor #-} import Control.Comonad import Data.List import Control.Monad data Zipper a = Zipper [a] a [a] deriving (Eq, Show, Functor) lefts, rights :: Zipper a -> [a] lefts (Zipper ls _ _) = ls rights (Zipper _ _ rs) = rs bwd :: Zipper a -> Maybe (Zipper a) bwd (Zipper [] _ _) = Nothing bwd (Zipper (l:ls) a rs) = Just $ Zipper ls l (a:rs) fwd :: Zipper a -> Maybe (Zipper a) fwd (Zipper _ _ []) = Nothing fwd (Zipper ls a (r:rs)) = Just $ Zipper (a:ls) r rs instance Comonad Zipper where extract (Zipper _ a _) = a duplicate z = Zipper (unfoldr (fmap (join (,)) . bwd) z) z (unfoldr (fmap (join (,)) . fwd) z)

Podemos distribuir listas si sabemos de antemano su longitud. Dado que las listas de Haskell pueden ser infinitas, debemos medir la longitud con posiblemente infinitos naturales perezosos. Una solución alternativa para medir la longitud sería utilizar una lista "guía" a lo largo de la cual podamos comprimir otras listas. Sin embargo, preferiría no asumir en las funciones de distribución que una lista ficticia de este tipo está siempre disponible.

data Nat = Z | S Nat length'' :: [a] -> Nat length'' = foldr (const S) Z distList :: Functor f => Nat -> f [a] -> [f a] distList Z fas = [] distList (S n) fas = (head <$> fas) : distList n (tail <$> fas)

Por supuesto, esto falla con las excepciones de tiempo de ejecución si nuestra suposición de longitud es incorrecta.

Podemos distribuir Zipper s distribuyendo sus enfoques y contextos, siempre que sepamos la longitud de los contextos:

distZipper :: Functor f => Nat -> Nat -> f (Zipper a) -> Zipper (f a) distZipper l r fz = Zipper (distList l (lefts <$> fz)) (extract <$> fz) (distList r (rights <$> fz))

Finalmente, podemos duplicar las Grid en la forma en que vimos antes, pero primero tenemos que determinar la forma de las Zipper internas. Como suponemos que todas las Zipper interiores tienen la misma forma, solo miramos la Zipper en el foco:

duplicateGrid :: Grid a -> Grid (Grid a) duplicateGrid grid@(Zipper _ (Zipper ls _ rs) _) = fmap (distZipper (length'' ls) (length'' rs)) $ duplicate $ fmap duplicate grid

Probar esto (como ya debe haberlo experimentado) es bastante horrible, y todavía no he podido revisar ni siquiera un caso de dos en dos a mano.

Sin embargo, tengo bastante confianza en la implementación anterior, ya que las definiciones están muy limitadas por los tipos.