Подтвердить что ты не робот

Comonadically найти все способы сосредоточиться на сетке

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

Сначала какой-то контекст (ха-ха). У меня есть zipper по непустым спискам.

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

mkZipper :: a -> [a] -> LZipper a
mkZipper = LZipper (Reverse [])

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

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)

Дублирование застежки-молнии показывает вам все способы, которыми вы могли бы смотреть на нее, с упором на то, как вы смотрите на нее в настоящее время.

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

Например:

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

(Я использую капители и звездочки, чтобы указать фокус точки на молнии.)


Я пытаюсь работать с двумерными сетками с фокусом, представленным как молния молнии. Каждая внутренняя молния представляет собой ряд сетки. Моя конечная цель - найти пути через сетку, перепрыгивая от соседа к соседству.

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

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

Примеры:

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

То, что я хочу, эквивалентно LZipper duplicate для сетки: функция, которая принимает сетку и создает сетку всех способов, которыми вы могли бы смотреть на сетку, с акцентом на текущий способ, которым вы глядя на него.

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

Что я ожидаю:

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

Я попробовал duplicateGrid = duplicate . duplicate. У этого есть правильный тип, но (предполагая, что я правильно интерпретировал вывод show, который я, вероятно, не сделал), он дает мне сетки, сосредоточенные где-то в первом столбце:

(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 | |
| +-------+ +-------+ +-------+ |
+-------------------------------+

Я также пробовал duplicateGrid = duplicate . fmap duplicate. Предполагая еще раз, что я способен интерпретировать вывод show, это дало мне то, что содержало неправильные сетки, и фокусы строк были несогласованы, так что перемещение вниз также привело бы вас к следующему:

(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 * |
| +-------+ +-------+ ********* |
+-------------------------------+

Похоже, это был бы простой вопрос для тех, кто знает, но это заставляет мою голову вращаться. Полагаю, я мог бы запустить кривую функцию, которая называет up, down, left и right, но я чувствую, что comonadic машины должны быть в состоянии сделать это для меня. Какова правильная реализация duplicateGrid?

4b9b3361

Ответ 1

Здесь немного проблемы, мы пытаемся составить Grid с собой, потому что эта настройка дает нам слишком много неправильных способов реализации duplicate с правильным типом. Полезно рассмотреть общий случай, когда составленные комонады не обязательно одинаковы.

Предположим, что мы имеем f и g comonads. Тип duplicate становится:

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

Мы можем использовать следующие экземпляры Comonad:

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

Отсюда становится очевидным, что нам нужно поменять местами f и g посередине.

Существует класс типа Distributive, который имеет желаемый метод.

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

В частности, нам нужно реализовать Distributive g, а затем duplicate для составленного comonad можно реализовать как:

duplicate = fmap distribute . duplicate . fmap duplicate

Однако в документации в Distributive указано, что значения g должны иметь одинаковую форму, поэтому мы можем закрепить произвольное количество копий без потери информации.

Чтобы проиллюстрировать это, если Vec n a является вектором с n, тогда distribute :: [Vec n a] -> Vec n [a] является просто транспозицией матрицы. Нужно заранее направить вниз размер внутреннего вектора, потому что транспозиция на "оборванной" матрице должна опустить некоторые элементы, а это не законное поведение. Бесконечные потоки и молнии также распределяют штраф, так как у них тоже есть только один возможный размер.

Zipper не является законным Distributive, потому что Zipper содержит значения с контекстами разного размера. Тем не менее, мы можем реализовать ненадлежащее распределение, которое предполагает равномерные размеры контекста.

Ниже я реализую duplicate для Grid с точки зрения неправильного распределения для базовых списков.

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

Я собираюсь пропустить Reverse, чтобы уменьшить синтаксический шум; Надеюсь, вы извините меня.

{-# 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)

Мы можем распространять списки, если мы знаем их длину заранее. Поскольку списки Haskell могут быть бесконечными, мы должны измерять длину с возможными бесконечными ленивыми натуралами. Альтернативным решением для измерения длины будет использование "путевого" списка, по которому мы можем закрепить другие списки. Однако я предпочел бы не предполагать в функциях распределения, что такой список фиктивных программ всегда доступен.

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)

Конечно, это не выполняется с исключениями времени выполнения, если наше допущение длины неверно.

Мы можем распространять Zipper путем распространения их фокусов и контекстов при условии, что мы знаем длины контекстов:

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

Наконец, мы можем дублировать Grid так, как мы видели раньше, но сначала мы должны определить форму внутреннего Zipper s. Поскольку мы предполагаем, что все внутренние Zipper имеют одинаковую форму, мы смотрим только на Zipper в фокусе:

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

Тестирование этого (как вы должны были уже испытать) довольно ужасно, и я еще не успел проверить даже два дела на два.

Тем не менее, я уверен в этой реализации, поскольку определения сильно ограничены типами.

Ответ 2

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

Следовательно, вы заметите, что, хотя ваши функции up и down для Grid были полностью определены в терминах Молнии, вам нужно было использовать механизмы Traversable для определения left и right. Это также означает, что left и right не обладают теми же характеристиками производительности, что и up и down, так как вы "так противно против".

Поскольку ваш экземпляр Comonad был определен только с использованием ваших функций застежки-молнии, он может только duplicate в направлении, определяемом вашей молнией, а именно fwd и bwd (и по расширению up и down).

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

Если вы пытаетесь пересечь ваши молнии, как если бы они были похожими на любую другую структуру, вы будете получать Nothing с помощью duplicate. Обратите внимание, что произойдет, если вы действительно попытаетесь использовать ваши функции up, down, left, right в якобы не проблемном 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 bad...
*Main> down allRows >>= right
Nothing -- Still nothing

Перемещение right и left требует (как вы должным образом отмечаете с вашим упоминанием об инварианте), что каждый из ваших вспомогательных молний является однородным по структуре, иначе traverse выйдет из строя преждевременно. Это означает, что если вы действительно хотите использовать left и right, единственный способ, которым это будет играть с duplicate, - это использовать наиболее подходящий duplicate.

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

Альтернативой является использование только функций, которые поставляются с застежкой-молнией. Это означает только использование fwd и bwd, а затем extract фокуса и продолжение использования fwd и bwd, чтобы получить то же самое, что и left и right. Конечно, это означает отказ от возможности сказать "прямо вниз" и "вниз, а затем вправо", но, как мы уже видели, молнии не играют с несколькими путями хорошо.

Теперь давайте проверим ваши интуиции о том, как лучше всего интерпретировать то, что происходит с duplicate . duplicate $ myGrid. Хорошая площадь - это не лучший способ подумать о том, что происходит (и вы поймете, почему, если вы ограничиваете себя только extract и fwd и 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...

У нас действительно есть оборванная структура.

+---------------------------------------------------+
|                     ********* +-------+ +-------+ |
|                     * 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 |                     |
| +-------+ +-------+ +-------+                     |
+---------------------------------------------------+

Квадраты внутри этой оборванной структуры также не являются квадратами, они также будут оборваны. Эквивалентно вы могли бы думать о fwd как о подиуме. Или просто снимите молнии для 2-й структуры в целом.

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

Итак, мораль этой истории, молнии - это немного головная боль для 2-й структуры. (Idle думал: может быть, линзы могут быть интересными?)

Для любопытных мой подход ниже также работает только в том случае, если вы имеете в виду разрозненность структуры, с которой мы имеем дело; то есть fwd ing дважды, а затем извлечение даст вам эквивалент того, что хочет OP в нижнем правом углу его сетки, а не в нижней левой части.

Оригинал

Итак, вам нужно каким-то образом переключиться между вашим чистым duplicate и вашим Traversable дублированием. Самый простой способ - взять вашу функцию duplicate, которую вы уже написали, и просто добавить traverse в середине.

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

Теперь, когда у нас есть более общий duplicateT, мы можем избавиться от некоторого неприятного дублирования кода, переопределив duplicate в вашем экземпляре Comonad:

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

Затем вы получите то, что хотите

duplicateGrid = duplicate . duplicateT

Или, если вы хотите переключить порядок столбцов и строк, вы можете сделать обратное.

Примечание. Было бы даже приятнее, если бы Haskell позволил вам изначально определять ограничения типов на классы типов, чтобы у вас могли быть разные экземпляры Comonad (возможно, опосредованные с newtype) для вашего LZipper, которые меняют направление вашего duplicate. Проблема в том, что вам нужно что-то вроде instance Comonad LZipper (LZipper a) where ... или эквивалентного newtype, который вы просто не можете записать в Haskell. Возможно, вы могли бы сделать что-то вроде this с семействами типов, но я подозреваю, что это, вероятно, слишком велико для этого конкретного экземпляра.

Изменить. На самом деле вам даже не нужно duplicateT, если вы предоставите соответствующий экземпляр Applicative для 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)

Теперь просто возьмите исходный duplicate, который у вас был раньше, и используйте traverse.

duplicateGrid = duplicate . (traverse duplicate)

Ответ 3

Таким образом, существует тесно связанная комонада, которая может помочь вам. Мы имеем:

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) 

Таким образом, двунаправленный бесконечный массив будет MC (Sum Integer) a, а двунаправленная бесконечная сетка будет MC (Sum Integer, Sum Integer) a. И, конечно, MC m (MC n a) изоморфен MC (m,n) a через currying.

Во всяком случае, ваша желаемая функция дублирования сетки будет аналогичной (игнорируя обертки newtype и currying):

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

duplicate для массива 1D выглядит так:

duplicate f x y = f (x+y)

Итак, duplicate . duplicate:

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

Не то, что нужно. Что выглядит fmap duplicate:

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

Ясно, что выполнение duplicate снова даст нам то же самое, что duplicate . duplicate (что должно быть, поскольку это закон комонады). Тем не менее, это немного более многообещающе. Если бы мы сделали два fmap...

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

Теперь, если бы мы сделали duplicate, мы получили бы

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

Но это все еще неправильно. Изменение имен переменных, f (x+y) (dx + dy). Поэтому нам нужно что-то менять по двум внутренним переменным... Название теории категорий для того, что мы хотим, является дистрибутивным законом. Имя Haskell Traversable. Что выглядит sequenceA для функций (функции образуют функтор Applicative и на самом деле a Monad, монада Reader)? Тип говорит все.

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

Итак, наконец:

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)

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