Как сделать двоичную древовидную застежку-молнию экземпляром Comonad?

Я хочу сделать двоичное дерево застежкой-молнией экземпляр comonad, но я не могу понять, как правильно реализовать duplicate.

Вот моя попытка:

{-# LANGUAGE DeriveFunctor #-}
import Data.Function
import Control.Arrow
import Control.Comonad

data BinTree a
    = Leaf a
    | Branch a (BinTree a) (BinTree a)
      deriving (Functor, Show, Eq)

data Dir = L | R
    deriving (Show, Eq)

-- an incomplete binary tree, aka data context
data Partial a = Missing Dir (BinTree a) a
    deriving (Show, Eq, Functor)

-- BTZ for BinTree Zipper
newtype BTZ a = BTZ { getBTZ :: ([Partial a], BinTree a) }
    deriving (Show, Eq)

instance Functor BTZ where
    fmap f (BTZ (cs,t)) = BTZ (map (fmap f) cs, fmap f t)

-- | replace every node label with the zipper focusing on that node
dup :: BinTree a -> BinTree (BTZ a)
dup (Leaf v) = Leaf (BTZ ([], Leaf v))
dup [email protected](Branch v tl tr) = Branch (BTZ ([],t)) tlZ trZ
    where
        tlZ = fmap (BTZ . first (++ [Missing L tr v]) . getBTZ) (dup tl)
        trZ = fmap (BTZ . first (++ [Missing R tl v]) . getBTZ) (dup tr)

-- | extract root label
rootVal :: BinTree a -> a
rootVal (Leaf v) = v
rootVal (Branch v _ _) = v

-- | move zipper focus around
goUp, goLeft, goRight :: BTZ a -> BTZ a

goUp (BTZ ([], _)) = error "already at root"
goUp (BTZ (Missing wt t2 v:xs, t1)) = case wt of
    L -> BTZ (xs, Branch v t1 t2)
    R -> BTZ (xs, Branch v t2 t1)

goLeft z = let (cs,t) = getBTZ z in
    case t of
      Leaf _ -> error "already at leaf"
      Branch v t1 t2 -> BTZ (Missing L t2 v:cs, t1)

goRight z = let (cs,t) = getBTZ z in
    case t of
      Leaf _ -> error "already at leaf"
      Branch v t1 t2 -> BTZ (Missing R t1 v:cs, t2)

instance Comonad BTZ where
    extract (BTZ (_,t)) =
        case t of
          Leaf v -> v
          Branch v _ _ -> v

    duplicate [email protected](BTZ (cs, bt)) = case bt of
        Leaf _ -> BTZ (csZ, Leaf z) -- extract . duplicate = id
        Branch v tl tr ->
            -- for each subtree, use "dup" to build zippers,
            -- and attach the current focusing root(bt) and rest of the data context to it
            let tlZ = fmap (BTZ . first (++Missing L tr v :cs) . getBTZ) (dup tl)
                trZ = fmap (BTZ . first (++Missing R tl v :cs) . getBTZ) (dup tr)
             in BTZ (csZ, Branch z tlZ trZ)
        where
            -- go up and duplicate, we'll have a "BTZ (BTZ a)"
            -- from which we can grab "[Partial (BTZ a)]" out
            -- TODO: not sure if it works
            upZippers = take (length cs-1) . tail $ iterate goUp z
            csZ = fmap (head . fst . getBTZ . duplicate) upZippers

main :: IO ()
main = do
   let tr :: BTZ Int
       tr = rootVal $ dup (Branch 1 (Leaf 2) (Branch 3 (Leaf 4) (Leaf 5)))
       equalOnTr :: Eq a => (BTZ Int -> a) -> (BTZ Int -> a) -> Bool
       equalOnTr = (==) `on` ($ tr)
   print $ (extract . duplicate)      `equalOnTr` id
   print $ (fmap extract . duplicate) `equalOnTr` id
   print $ (duplicate . duplicate)    `equalOnTr` (fmap duplicate . duplicate)

Некоторое объяснение:

  • BinTree a - это тип данных двоичного дерева, и каждое дерево node содержит метку.
  • Partial a - это двоичное дерево с левым или правым поддеревом. Стек Partial a в моем коде играет роль контекста данных.
  • BTZ обозначает застежку BinTree, которую я хочу сделать экземпляром Comonad, она состоит из контекста данных и поддерева фокусировки.

Чтобы сделать его экземпляром Comonad, я планирую реализовать extract и duplicate и проверить, сохранены ли свойства comonad, взяв некоторые случайные двоичные деревья.

extract прост, просто вынимая поддеревье фокусировки.

Функция dup служит вспомогательной функцией, которая заменяет каждую метку node с застежкой на дереве, фокусирующей на этом node.

Для duplicate z метка node должна быть самой z, так что extract . duplicate == id выполняется. Для не-листовых узлов я использую dup для работы с их поддеревьями, как если бы у них не было родителей, а текущий фокус z и остальная часть контекста данных добавляются к этим застежкам-молчетам впоследствии.

До сих пор выполнялись первые два свойства comonad (extract . duplicate = id и fmap extract . duplicate), но я не знаю, что делать с контекстом данных. То, что я сейчас делаю, это сделать молнию z и продолжать расти. По пути мы берем верхнюю часть каждого стека контекста данных для создания нового стека контекста данных, который звучит правильно, а также имеет правильный тип ([Partial (BTZ a)]. Но мой подход не может удовлетворить третий закон.

Учитывая определение типа данных бинарного дерева на молнии выше, возможно ли сделать это примером Комонада? Если ответ "да", что-то не так с моим подходом?

Ответ 1

В дифференциальном исчислении обозначение Лейбница вызывает меньше путаницы, чем Ньютон, потому что оно явно относится к переменной, по которой мы дифференцируем. Контексты в вещах даются дифференциацией, поэтому мы должны позаботиться о том, что является контекстуализированным. Здесь есть два понятия "подструктуры": поддеревья и элементы. У каждого из них разные (но связанные) понятия "контекст" и, следовательно, "застежка-молния", где застежка-молния - это пара вещи и ее контекст.

Тип BTZ представлен как понятие молнии для поддеревьев. Тем не менее, застежка-молния, выполненная из конопличной конструкции, работает на застежках-молниях для элементов: extract означает "дать элемент здесь"; duplicate означает "украсить каждый элемент своим контекстом". Поэтому вам нужны контексты элементов. Смутно, что для этих двоичных деревьев застежки-молнии и подделки застежки-молнии изоморфны, но это по какой-то особой причине (а именно, что они образуют cofree comonad).

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

Подсвечники контекста

Подсвечники-контексты для [a] задаются только [a], являясь списком элементов, которые мы проходим, по пути от подсписок до всего списка. Подсвещенный контекст для [3,4] в [1,2,3,4] равен [2,1]. Контексты субнода для рекурсивных данных всегда представляют собой списки, представляющие то, что вы видите на пути от node к корню. Тип каждого шага задается частичной производной формулы для одного node данных относительно рекурсивной переменной. Итак, здесь

[a] = t where             -- t is the recursive variable standing for [a]
  t = 1 + a*t             -- lists of a are either [] or an (a : t) pair
∂/∂t (1 + a*t) = a        -- that one step on a path from node to root
sublist contexts are [a]  -- a list of such steps

Итак, подслист-молния представляет собой пару

data LinLZ a = LinLZ
  {  subListCtxt  :: [a]
  ,  subList      :: [a]
  }

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

plugLinLZ :: LinLZ a -> [a]
plugLinLZ (LinLZ { subListCtxt = [],      subList = ys})  = ys
plugLinLZ (LinLZ { subListCtxt = x : xs,  subList = ys})
  = plugLinLZ (LinLZ { subListCtxt = xs,  subList = x : ys})

Но мы не можем сделать LinLZ a Comonad, потому что (например) из

LinLZ { subListCtxt = [], subList = [] }

мы не можем extract использовать элемент (a a из LinLZ a), только подстрочный.

Контексты элементов списка

Контекст элемента списка - это пара списков: элементы перед элементом в фокусе и элементы после него. Контекст элемента в рекурсивной структуре всегда представляет собой пару: сначала дайте контекст subnode для поднода, где хранится этот элемент, затем укажите контекст для элемента в его node. Мы получаем контекст element-in-its-node, дифференцируя формулу для a node относительно переменной, которая обозначает элементы.

[a] = t where             -- t is the recursive variable standing for [a]
  t = 1 + a*t             -- lists of a are either [] or an (a : t) pair
∂/∂a (1 + a*t) = t = [a]  -- the context for the head element is the tail list

Итак, контекст элемента задается парой

type DL a =
  (  [a]     -- the sublist context for the node where the element is
  ,  [a]     -- the tail of the node where the element is
  )

и элементная молния задается путем спаривания такого контекста с элементом "в отверстии".

data ZL a = ZL
  {  this :: a
  ,  between :: DL a
  }  deriving (Show, Eq, Functor)

Вы можете превратить такую ​​застежку-молнию в список ( "выходить" из элемента), сначала воссоздавая подсписку, где сидит элемент, давая нам подслистскую застежку-молнию, а затем подключая подсписку в свой контекст-подсвет.

outZL :: ZL a -> [a]
outZL (ZL { this = x, between = (zs, xs) })
  = plugLinLZ (LinLZ { subListCtxt = zs, subList = x : xs })

Вставка каждого элемента в контекст

Учитывая список, мы можем связать каждый элемент с его контекстом. Мы получаем список способов "войти" в один из элементов. Мы начинаем вот так:

into :: [a] -> [ZL a]
into xs = moreInto (LinLZ { subListCtxt = [], subList = xs })

но реальная работа выполняется вспомогательной функцией, которая работает в списке-в-контексте.

moreInto :: LinLZ a -> [ZL a]
moreInto (LinLZ { subListCtxt = _,   subList = [] })      = []
moreInto (LinLZ { subListCtxt = zs,  subList = x : xs })
  =  ZL { this = x, between = (zs, xs) } 
  :  moreInto (LinLZ { subListCtxt = x : zs,  subList = xs })

Обратите внимание, что выход эхосигнала соответствует форме текущего subList. Кроме того, молния в x имеет место this = x. Кроме того, генерирующая молния для декорирования xs имеет subList = xs и правильный контекст, что мы прошли мимо x. Тестирование,

into [1,2,3,4] =
  [  ZL {this = 1, between = ([],[2,3,4])}
  ,  ZL {this = 2, between = ([1],[3,4])}
  ,  ZL {this = 3, between = ([2,1],[4])}
  ,  ZL {this = 4, between = ([3,2,1],[])}
  ]

Комонадная структура для элементов списка элементов

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

instance Comonad ZL where

extract дает нам элемент, который мы посещаем.

  extract = this

В duplicate застежку-молнию заменим текущий элемент x на всю текущую молнию zl (чья this = x)...

  duplicate [email protected](ZL { this = x, between = (zs, ys) }) = ZL
    {  this = zl

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

    ,  between =
         (  outward (LinLZ { subListCtxt = zs, subList = x : ys })
         ,  moreInto (LinLZ { subListCtxt = x : zs, subList = ys })
         )
    }

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

    where
      outward (LinLZ { subListCtxt = [], subList = _ }) = []
      outward (LinLZ { subListCtxt = z : zs, subList = ys })
        =  ZL { this = z, between = (zs, ys) }
        :  outward (LinLZ { subListCtxt = zs, subList = z : ys })

Итак, получим

duplicate ZL {this = 2, between = ([1],[3,4])}
  = ZL
  {  this = ZL {this = 2, between = ([1],[3,4])}
  ,  between =
     (  [  ZL {this = 1, between = ([],[2,3,4])}  ]
     ,  [  ZL {this = 3, between = ([2,1],[4])}
        ,  ZL {this = 4, between = ([3,2,1],[])}
        ]
     )
  }

где this находится в 2 ", и мы between" переходим к 1 "и" переходим к 3 или переходим к 4 ".

Итак, структура comonadic показывает нам, как мы можем перемещаться между разными элементами, находящимися внутри списка. Подсветовая структура играет ключевую роль в поиске узлов, где находятся элементы, но структура молнии duplicate d является элементом застежки-молнии.

А как насчет деревьев?

Отступление: отмеченные деревья уже есть comonads

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

data TF t = Leaf | Fork (t, t) deriving (Show, Eq, Functor)
data BT a = a :& TF (BT a) deriving (Show, Eq, Functor)

То есть, каждое дерево node имеет метку, будь то лист или вилка.

Если у нас есть структура, в которой каждая node имеет метку и blob подструктур, мы имеем comonad: cofree comonad. Позвольте мне реорганизовать немного больше, абстрагируя TF...

data CoFree f a = a :& f (CoFree f a) deriving (Functor)

... так что мы имеем общий f, где мы имели TF раньше. Мы можем восстановить наши конкретные деревья.

data TF t = Leaf | Fork (t, t) deriving (Show, Eq, Functor)
type BT = CoFree TF
deriving instance Show a => Show (BT a)
deriving instance Eq a => Eq (BT a)

И теперь, раз и навсегда, мы можем дать конструкцию cofree comonad. Поскольку каждое поддерево имеет корневой элемент, каждый элемент может быть украшен деревом, корень которого он есть.

instance Functor f => Comonad (CoFree f) where
  extract   (a :& _)     = a                         -- extract root element
  duplicate [email protected](a :& ft)  = t :& fmap duplicate ft    -- replace root element by whole tree

Приведем пример

aTree =
  0 :& Fork
  (  1 :& Fork
     (  2 :& Leaf
     ,  3 :& Leaf
     )
  ,  4 :& Leaf
  )

duplicate aTree =
  (0 :& Fork (1 :& Fork (2 :& Leaf,3 :& Leaf),4 :& Leaf)) :& Fork
  (  (1 :& Fork (2 :& Leaf,3 :& Leaf)) :& Fork
     (  (2 :& Leaf) :& Leaf
     ,  (3 :& Leaf) :& Leaf
     )
  ,  (4 :& Leaf) :& Leaf
  )

См? Каждый элемент соединен с его поддеревом!

Списки не вызывают cofree comonad, потому что не каждый node имеет элемент: в частности, [] не имеет элемента. В cofree comonad всегда есть элемент, где вы находитесь, и вы можете видеть дальше в древовидную структуру, но не дальше.

В элементе zipper comonad всегда есть элемент, где вы находитесь, и вы можете видеть вверх и вниз.

Субтитры и контексты элементов в бинарных деревьях

алгебраически

d/dt (TF t) = d/dt (1 + t*t) = 0 + (1*t + t*1)

поэтому мы можем определить

type DTF t = Either ((), t) (t, ())

говоря, что поддерево внутри "blob подструктур" находится либо слева, либо справа. Мы можем проверить, работает ли "подключить".

plugF :: t -> DTF t -> TF t
plugF  t  (Left   ((), r))  = Fork (t, r)
plugF  t  (Right  (l, ()))  = Fork (l, t)

Если мы создадим экземпляр t и сопоставим с меткой node, мы получим один шаг контекста поддерева

type BTStep a = (a, DTF (BT a))

который изоморфен Partial в вопросе.

plugBTinBT :: BT a -> BTStep a -> BT a
plugBTinBT t (a, d) = a :& plugF t d

Итак, поддерево-контекст для одного BT a внутри другого задается [BTStep a].

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

data DBT a = DBT
  {  below  :: TF (BT a)    -- the rest of the element node
  ,  above  :: [BTStep a]   -- the subtree context of the element node
  }  deriving (Show, Eq)

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

instance Functor DBT where
  fmap f (DBT { above = a, below = b }) = DBT
    {  below = fmap (fmap f) b
    ,  above = fmap (f *** (either
         (Left   . (id *** fmap f))
         (Right  . (fmap f *** id)))) a  
    }

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

data BTZ a = BTZ
  {  here  :: a
  ,  ctxt  :: DBT a
  }  deriving (Show, Eq, Functor)

Если вы думаете "что нового?", вы правы. У нас есть контекст поддерева, above, вместе с поддеревом, заданным here и below. И это потому, что единственными элементами являются те, которые обозначают узлы. Разделение node вверх на элемент и его контекст такое же, как разбиение его на его метку и ее блоб подструктур. То есть, мы получаем это совпадение для cofree comonads, но не в целом.

Однако это совпадение - только отвлечение! Как мы видели со списками, мы не нуждаемся в элементарных застежках-молниях, чтобы они были такими же, как и подзоны-молнии, чтобы сделать элементы-молнии комонадой.

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

down :: BT a -> BT (BTZ a)
down t = downIn t []

downIn :: BT a -> [BTStep a] -> BT (BTZ a)
downIn (a :& ft) ads =
  BTZ { here = a, ctxt = DBT { below = ft, above = ads } }
  :& furtherIn a ft ads

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

furtherIn :: a -> TF (BT a) -> [BTStep a] -> TF (BT (BTZ a))
furtherIn a Leaf           ads  = Leaf
furtherIn a (Fork (l, r))  ads  = Fork
  (  downIn l ((a, Left   ((), r)) : ads)
  ,  downIn r ((a, Right  (l, ())) : ads)
  )

Посмотрите, что furtherIn сохраняет древовидную структуру, но подходит к контексту поддерева, когда он посещает поддерево.

Пусть двойная проверка.

down aTree =
  BTZ {  here  = 0, ctxt = DBT {
         below = Fork (1 :& Fork (2 :& Leaf,3 :& Leaf),4 :& Leaf),
         above = []}} :& Fork
  (  BTZ {  here = 1, ctxt = DBT {
            below = Fork (2 :& Leaf,3 :& Leaf),
            above = [(0,Left ((),4 :& Leaf))]}} :& Fork
     (  BTZ {  here = 2, ctxt = DBT {
               below = Leaf,
               above = [(1,Left ((),3 :& Leaf)),(0,Left ((),4 :& Leaf))]}} :& Leaf
     ,  BTZ {  here = 3, ctxt = DBT {
               below = Leaf,
               above = [(1,Right (2 :& Leaf,())),(0,Left ((),4 :& Leaf))]}} :& Leaf
     )
  ,  BTZ {  here = 4, ctxt = DBT {
            below = Leaf,
            above = [(0,Right (1 :& Fork (2 :& Leaf,3 :& Leaf),()))]}} :& Leaf)

См? Каждый элемент имеет весь контекст, а не только дерево под ним.

Двоичные застежки-молнии образуют Comonad

Теперь, когда мы можем украсить элементы их контекстами, создадим экземпляр Comonad. Как и раньше...

instance Comonad BTZ where
  extract = here

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

  duplicate [email protected](BTZ { here = a, ctxt = DBT { below = ft, above = ads }}) = BTZ
    {  here = z
    ,  ctxt = DBT
         {  below = furtherIn a ft ads  -- move somewhere below a
         ,  above = go_a (a :& ft) ads  -- go above a
         }
    } where

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

    go_a t []          = []
    go_a t (ad : ads)  = go_ad t ad ads : go_a (plugBTinBT t ad) ads
    go_ad t (a, d) ads =
      (  BTZ { here = a, ctxt = DBT { below = plugF t d, above = ads } }  -- visit here
      ,  go_d t a d ads                                                   -- try other subtree
      )

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

    go_d t a (Left ((), r)) ads = Left ((), downIn r ((a, Right (t, ())) : ads))
    go_d t a (Right (l, ())) ads = Right (downIn l ((a, Left ((), t)) : ads), ())

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

Посмотрим. Здесь мы посетили 1:

duplicate (BTZ {here = 1, ctxt = DBT {
                below = Fork (2 :& Leaf,3 :& Leaf),
                above = [(0,Left ((),4 :& Leaf))]}}) =
  BTZ {here = BTZ {here = 1, ctxt = DBT {
                   below = Fork (2 :& Leaf,3 :& Leaf),
                   above = [(0,Left ((),4 :& Leaf))]}}, ctxt = DBT {
       below = Fork (BTZ {here = 2, ctxt = DBT {
                          below = Leaf,
                          above = [(1,Left ((),3 :& Leaf)),(0,Left ((),4 :& Leaf))]}} :& Leaf
                    ,BTZ {here = 3, ctxt = DBT {
                          below = Leaf,
                          above = [(1,Right (2 :& Leaf,())),(0,Left ((),4 :& Leaf))]}} :& Leaf
                   ),
       above = [(BTZ {here = 0, ctxt = DBT {
                      below = Fork (1 :& Fork (2 :& Leaf,3 :& Leaf),4 :& Leaf),
                      above = []}}
                ,Left ((),BTZ {here = 4, ctxt = DBT {
                               below = Leaf,
                               above = [(0,Right (1 :& Fork (2 :& Leaf,3 :& Leaf),()))]}} :& Leaf)
                )
               ]}}

В целях тестирования законов comonad на небольшой выборке данных, проверим:

fmap (\ z -> extract (duplicate z) == z) (down aTree)
  = True :& Fork (True :& Fork (True :& Leaf,True :& Leaf),True :& Leaf)
fmap (\ z -> fmap extract (duplicate z) == z) (down aTree)
  = True :& Fork (True :& Fork (True :& Leaf,True :& Leaf),True :& Leaf)
fmap (\ z -> fmap duplicate (duplicate z) == duplicate (duplicate z)) (down aTree)
  = True :& Fork (True :& Fork (True :& Leaf,True :& Leaf),True :& Leaf)