haskell - 如何使二叉树 zipper 成为Comonad的实例?

标签 haskell zipper comonad

我想使二叉树 zipper 成为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 t@(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 z@(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是二叉树数据类型,每个树节点都包含一个标签。
  • Partial a是带有左或右子树的二叉树。一堆Partial a在我的代码中扮演数据上下文的角色。
  • BTZ代表BinTree zipper ,我要创建Comonad的实例,它由数据上下文和焦点子树组成。

  • 为了使其成为Comonad的实例,我的计划是实现extractduplicate,并通过采用一些随机的二叉树来验证comonad属性是否成立。
    extract很简单,只需取出聚焦子树即可。

    函数dup用作替换每个节点标签的辅助函数
    树型 zipper 专注于该节点。

    对于duplicate z,节点标签本身必须是z,以便extract . duplicate == id成立。对于非叶节点,我使用dup来处理它们的子树,就好像它们没有父级一样,并且z当前的关注点和其余数据上下文随后将附加到这些 zipper 中。

    到目前为止,前两个comonad属性已经满足(extract . duplicate = idfmap extract . duplicate),但是我不知道该如何处理数据上下文。我目前正在做的是 zipper z并保持增长。在此过程中,我们以每个数据上下文堆栈的顶部为基础来构建新的数据上下文堆栈,这听起来很正确,并且类型正确([Partial (BTZ a)]。)但是我的方法不能满足第三定律。

    给定上面的二叉树 zipper 的数据类型定义,
    是否可以使其成为Comonad的实例?
    如果答案是肯定的,我的方法是否有问题?

    最佳答案

    在微积分中,莱布尼兹的概念引起的混淆比牛顿的引起的少,这是因为它清楚地表明了我们要区分的变量。事物中的上下文是通过差异给出的,因此我们必须注意上下文中的内容。这里,有两个“子结构”概念在起作用:子树和元素。它们各自具有“上下文”和“ zipper ”的不同(但相关)概念,其中 zipper 是事物及其上下文的对。

    您的BTZ类型表示为子树的zipper概念。但是, zipper comonadic构造在元素的 zipper 上起作用:extract表示“在此处提供元素”; duplicate的意思是“用上下文装饰每个元素”。因此,您需要元素上下文。令人困惑的是,对于这些二叉树,元素 zipper 和子树 zipper 是同构的,但这是出于一个非常特殊的原因(即它们形成了共自由共态)。

    通常,例如对于列表,元素和子树 zipper 是不同的。如果我们从构建列表的元素 zipper 开始,那么当我们回到树上时,我们就不太可能迷路。让我也尝试为他人以及您自己补充一些概况。

    子列表上下文
    [a]只是给出了[a]的子列表上下文,是我们从子列表到整个列表的途中经过的元素列表。 [3,4][1,2,3,4]的子列表上下文是[2,1]。递归数据的子节点上下文始终是代表您在从节点到根的路径上看到的内容的列表。相对于递归变量,每个步骤的类型由一个数据节点的公式的偏导数给出。所以在这里

    [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's one step on a path from node to root
    sublist contexts are [a]  -- a list of such steps
    

    因此,子列表 zipper 是一对
    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设为Comonad,因为(例如)来自
    LinLZ { subListCtxt = [], subList = [] }
    

    我们不能extract一个元素(a中的LinLZ a),只能是一个子列表。

    列表元素上下文

    列表元素上下文是一对列表:焦点所在的元素之前的元素,以及后面的元素。递归结构中的元素上下文始终是一对:首先给出存储元素的子节点的subnode-context,然后给出其节点中元素的上下文。我们通过相对于代表元素的变量区分节点的公式来获得节点中元素的上下文。
    [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
      )
    

    通过将此类上下文与“在洞中”元素配对来给出元素 zipper 。
    data ZL a = ZL
      {  this :: a
      ,  between :: DL a
      }  deriving (Show, Eq, Functor)
    

    通过首先重构元素所在的子列表,为我们提供一个子列表 zipper ,然后将子列表插入其子列表上下文,可以将这样的 zipper 变回列表(从元素中“移出”)。
    outZL :: ZL a -> [a]
    outZL (ZL { this = x, between = (zs, xs) })
      = plugLinLZ (LinLZ { subListCtxt = zs, subList = x : xs })
    

    将每个元素置于上下文中

    给定一个列表,我们可以将每个元素与其上下文配对。我们获得了可以“进入”要素之一的方式的 list 。我们这样开始
    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位置的 zipper 具有this = x。另外,用于装饰xs的生成 zipper 具有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],[])}
      ]
    

    列表元素 zipper 的共通结构

    我们已经了解了如何从一个元素进入或进入一个可用元素。共鸣结构告诉我们如何在元素之间移动,或者停留在我们所处的位置,或者移动到其他元素。
    instance Comonad ZL where
    
    extract为我们提供了我们要访问的元素。
      extract = this
    

    为了duplicate一个 zipper ,我们用整个当前的zip x(其zl)替换当前元素this = x ...
      duplicate zl@(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”。

    因此,共声结构向我们展示了如何在列表内的不同元素之间移动。子列表结构在查找元素所在的节点中起着关键作用,但是 zipper 结构duplicate d是元素 zipper 。

    那树呢?

    题外话:标有树木的树已成冠

    让我重构您的二叉树类型,以得出一些结构。从字面上看,让我们拉出标记叶子或 fork 的元素是一个共同因素。让我们也隔离解释该叶 fork 树结构的函子(TF)。
    data TF t = Leaf | Fork (t, t) deriving (Show, Eq, Functor)
    data BT a = a :& TF (BT a) deriving (Show, Eq, Functor)
    

    也就是说,每个树节点都有一个标签,无论它是叶子还是 fork 。

    无论我们在每个节点都有一个标签和一个子结构的结构的任何地方,都有一个共性:cofree共性。让我多重构一点,抽象出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 t@(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,因为不是每个节点都有一个元素:具体地说,[]没有元素。在舒适的社区中,总有一个元素在您所在的位置,您可以看到更深的树状结构,但看不到更深的树状结构。

    在元素 zipper 组合中,总有一个元素在您所在的位置,并且可以同时看到上下两个元素。

    二叉树中的子树和元素上下文

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

    所以我们可以定义
    type DTF t = Either ((), t) (t, ())
    

    说“子结构块”内部的子树在左侧或右侧。我们可以检查“插入”是否有效。
    plugF :: t -> DTF t -> TF t
    plugF  t  (Left   ((), r))  = Fork (t, r)
    plugF  t  (Right  (l, ()))  = Fork (l, t)
    

    如果我们实例化t并与节点标签配对,我们将获得子树上下文的一步
    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's node
      ,  above  :: [BTStep a]   -- the subtree context of the element's 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  
        }
    

    现在我可以说元素 zipper 是什么。
    data BTZ a = BTZ
      {  here  :: a
      ,  ctxt  :: DBT a
      }  deriving (Show, Eq, Functor)
    

    如果您在想“有什么新变化?”,那是对的。我们有一个子树上下文above,以及一个由herebelow给出的子树。那是因为唯一的元素是那些标记节点的元素。
    将节点拆分为一个元素及其上下文与将其拆分为其标签及其子结构 Blob 相同。也就是说,对于cofree comonads,我们会得到这种巧合,但通常不会。

    但是,这种巧合只是分心!正如我们在列表中看到的那样,我们不需要element-zippers与subnode-zippers相同就可以使element-zippers成为共同对象。

    按照与上面列表相同的模式,我们可以用其上下文装饰每个元素。这项工作由帮助程序功能完成,该功能积累了我们当前正在访问的子树上下文。
    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的 zipper 。子树由另一个助手处理。
    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)
    

    看到?每个元素都以其整个上下文进行装饰,而不仅仅是下面的树。

    二叉树 zipper 形成了Comonad

    现在我们可以用它们的上下文来装饰元素,让我们构建Comonad实例。像之前一样...
    instance Comonad BTZ where
      extract = here
    

    ... extract告诉我们重点所在的元素,我们可以利用我们现有的机制来深入研究这棵树,但是我们需要构建新的工具包来探索向外移动的方式。
      duplicate z@(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)
                    )
                   ]}}
    

    通过对少量数据样本测试共同定律,让我们检查:
    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)
    

    关于haskell - 如何使二叉树 zipper 成为Comonad的实例?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25519767/

    相关文章:

    xml - 用于创建 xml 请求的 zipper ?

    python - 什么是 python 字典和 zip 的最佳 C++ 替代品?

    使用 Store comonad 的 Conway 生命游戏的性能

    haskell - Comonad 重复功能

    haskell - comonad 是否适合为 Wumpus 世界建模?

    datetime - Haskell函数将日期的一部分作为字符串

    Haskell 到 Javascript 编译器?

    list - Haskell:编译函数与解释函数之间的奇怪差异,这些函数打印连接的无限列表

    haskell - 为什么我注释这个类型签名的错误没有破坏东西?

    Clojure zipper 路径功能不完整?