【问题标题】:Comonadically finding all the ways to focus on a grid [duplicate]共同寻找所有关注网格的方法[重复]
【发布时间】:2016-06-20 00:58:51
【问题描述】:
{-# 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 |
-- +-------+

我想要的是与LZipperduplicate 等效的网格:一个函数,它接受一个网格并生成一个你可以查看网格的所有方式的网格,重点放在 当前的你看待它的方式。

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

对于那些知道的人来说,这感觉像是一个简单的问题,但它让我头晕目眩。我想我可以手动启动一个调用updownleftright 的函数,但我觉得comonadic 机器应该能够为我做这件事。 duplicateGrid的正确实现是什么?

【问题讨论】:

  • 仅供参考,您可能对其他位感兴趣。见stackoverflow.com/a/25572148/1477667
  • this one 专门解决您的问题。我不知何故错过了它,而且我的答案相当多余,但至少我有机会自己弄清楚其中的一部分。
  • @AndrásKovács D'oh,我的问题似乎与那个问题完全相同。不知道为什么我在搜索时没有找到它。我会做正确的事并关闭这个。
  • @Benjamin Hodgson,您的问题更简单而且写得很好。它更容易理解和回答,所以我认为它不应该被关闭。
  • @user3237465 这个问题及其答案仍然在这里供后代搜索和阅读。只是不会有任何新的答案,现在有一个方便的链接指向重复的问题。

标签: haskell zipper comonad


【解决方案1】:

这里有点问题,我们试图用它自己来组合Grid,因为这种设置给了我们太多不正确的方法来实现具有正确类型的duplicate。考虑组合的共单子不一定相同的一般情况很有用。

假设我们有 fg 共胞。 duplicate 的类型变为:

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

我们可以单独使用Comonad 实例获得以下信息:

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

由此可见,我们需要在中间交换fg

有一个名为 Distributive 的类型类拥有我们想要的方法。

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

具体来说,我们需要实现Distributive g,然后组合comonad的duplicate可以实现为:

duplicate = fmap distribute . duplicate . fmap duplicate

但是,Distributive 中的文档说g 的值必须具有完全相同的形状,因此我们可以将任意数量的副本压缩在一起而不会丢失信息。

为了说明这一点,如果Vec n a 是一个n 大小的向量,那么distribute :: [Vec n a] -> Vec n [a] 就是矩阵转置。有必要事先确定内部向量的大小,因为在“参差不齐”的矩阵上的转置必须删除一些元素,这是不合法的行为。无限流和拉链也分布良好,因为它们也只有一种可能的尺寸。

Zipper 不是合法的Distributive,因为Zipper 包含具有不同大小上下文的值。尽管如此,我们仍然可以实现假设统一上下文大小的不正确分布。

下面我将为Grid 实现duplicate,以防止底层列表的不正确分布。

或者,也可以卷起袖子直接在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)

当然,如果我们的长度假设不正确,则会出现运行时异常。

如果我们知道上下文的长度,我们可以通过分发他们的焦点和上下文来分发Zippers:

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

最后,我们可以像之前看到的那样复制Grids,但首先我们要确定内部Zippers的形状。由于我们假设所有内部Zippers 的形状相同,所以我们只看焦点中的Zipper

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

测试这个(你一定已经经历过)非常糟糕,而且我还没有动手检查一个两两的情况。

不过,我对上述实现相当有信心,因为定义受到类型的高度限制。

【讨论】:

  • 为什么不假设“虚拟列表”可用?我相信只要你能写一个概括的replicate,你就会得到其中的一个,而且我认为你在做的事情在这种情况之外是没有意义的。
  • 我并没有考虑太多,我只是倾向于将最弱的假设作为习惯。既然你指出了这一点,我不得不想出一些复杂的反例。到目前为止,我想出了这个:如果f 是一个常量函子,那么我们应该能够distribute 具有任意长度。但是如果f [a] 中的a 是底部,我们只能将[] 生成为虚拟列表(以总语言形式)。
  • 您允许共单子不同的技巧很有启发性;我不认为GridZipper 与自身(以及相应的Comonad 实例)的composition。您复制两次然后分发的解决方案似乎与@pigworker's answer 一致,因为如果您可以访问更强的ApplicativeTraversable 约束——我们这样做——那么distribute 就是sequenceA
【解决方案2】:

您遇到的根本问题是zippers don't natively support 2-d structures。那里的答案很好(另一个答案基本上正是您对Grid 的定义),我鼓励您阅读它,但要点是拉链识别具有到达那里的路径和二维空间中的元素,例如识别是有问题的,因为有很多路径可以到达一个点。

因此您会注意到,虽然您的 Grids 的 updown 函数完全根据拉链定义,但您需要使用 Traversable 机器来定义 leftright。这也意味着leftright 无法享受与updown 相同的性能属性,因为可以这么说,您是在“违背常规”。

由于您的Comonad 实例仅使用您的拉链函数定义,因此它只能在您的拉链定义的方向上duplicate,即fwdbwd(以及扩展名up 和@ 987654339@)。

编辑:经过深思熟虑,我认为您的方法从根本上是有问题的。我在下面保留了我的原文,但还有一个更明显的问题。

如果您尝试像任何二维其他结构一样遍历您的拉链,您将不断获得Nothingduplicate。让我们注意如果您实际上尝试在表面上没有问题的 duplicate (mkZipper 'a' "bc") 上使用您的 up, down, left, right 函数会发生什么。

*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

移动rightleft 要求(正如您在提到不变量时应注意的那样)您的每个子拉链在结构上都是同质的,否则traverse 将过早失效。这意味着,如果你真的想使用leftright,那么与duplicate 搭配使用的唯一方法就是尽可能使用最统一的duplicate

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

另一种方法是只使用拉链自带的功能。这意味着只使用fwdbwd,然后extract 聚焦并继续使用fwdbwd 以获得与leftright 相同的东西。当然,这意味着放弃说“先右后下”和“先下后右”的能力,但正如我们已经看到的那样,拉链不能很好地处理多条路径。

现在让我们再次检查您的直觉,了解如何最好地解释 duplicate . duplicate $ myGrid 发生的事情。一个漂亮的正方形并不是思考正在发生的事情的最佳方式(如果你将自己限制在extractfwdbwd,你就会明白为什么。

*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 视为对角线。或者干脆完全放下二维结构的拉链。

根据我的经验,拉链与树状物体搭配使用效果最好。如果 Haskell 专家能想出一种使用拉链的方法以及所有更新/访问它们带来的好处,比如循环图,甚至只是普通的旧 DAG,但我想不出任何东西 :)。

故事的寓意如此,拉链对于二维结构来说相当令人头疼。 (闲想:也许镜头会很有趣?)

出于好奇,我下面的方法也只有在您牢记我们正在处理的结构的粗糙时才有效;即fwding 两次,然后提取将获得 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's the exact same except for that extra traverse
    where step move = fmap (\y -> (y, y)) . (traverse move)

现在我们有了一个更通用的duplicateT,我们可以通过在Comonad 实例中将duplicate 重新定义为:

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

那么下面的内容就可以得到你想要的

duplicateGrid = duplicate . duplicateT

或者如果你想切换列和行的顺序,你可以做相反的事情。

注意:如果 Haskell 允许您在类型类上本地定义类型约束,这样您就可以为您的 newtypes 拥有不同的 Comonad 实例(可能都以 newtypes 为中介)来改变您的方向duplicate。问题是您想要instance Comonad LZipper (LZipper a) where ... 或等效的newtype 之类的东西,而您根本无法用Haskell 编写它们。可以想象,您可以对类型族执行类似 this 的操作,但我怀疑这对于这个特定实例来说可能是矫枉过正。

编辑:事实上,如果您为LZipper 提供适当的Applicative 实例,您甚至不需要duplicateT

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】:

    因此,有一个密切相关的comonad 可以帮助指导您。我们有:

    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 同构。

    无论如何,您想要的重复网格函数将类似于(忽略新类型包装器和柯里化):

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

    duplicate 的一维数组看起来像:

    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 相同的东西(它应该是因为这是comonad 法则)。然而,这更有希望。如果我们做了两个 fmaps ...

    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 函子,实际上是MonadReader monad)是什么样的?类型说明一切。

    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)
    

    我实际上没有尝试过类似的代码,所以我不知道它是否有效,但数学表明它应该。

    【讨论】:

      猜你喜欢
      • 2018-04-24
      • 2021-02-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多