哪种代数模式适合这种类型的树?

时间:2017-03-19 19:22:20

标签: haskell monads comonad

我有一个难题,

我设法编写了一些使用递归方案来做这些事情的代码,但是它非常混乱 这通常意味着我错过了一个有用的抽象。

我正在为我的文本编辑器设计布局系统 Rasa;它使用非常相似的分裂 作为Vim的态度。我决定使用树来描述分裂;你可以想象 它作为垂直或水平分割的二叉树,具有“视图”和“视图”。在 叶节点。 This picture 可能有所帮助。

这是我的初始数据结构:

data Direction = Hor | Vert
data Tree a = 
  Branch Direction (Tree a) (Tree a)
  | Leaf a
  deriving (Functor)

我需要的一些操作是:

  • split :: (View -> Tree View) -> Tree View -> Tree View 将节点(或不是)水平或垂直地分成两个节点(同时保持它们的位置) 树)
  • close :: (View -> Bool) -> Tree View -> Tree View'关闭'通过删除匹配谓词的任何视图 它们来自树,并正确地重新组织相邻的视图。
  • fmap;我希望树能成为一个仿函数,所以我可以改变观点。

有一些很好的功能: - focusRight :: Tree View -> Tree View,当且仅当最近的水平连接时,将视图设置为活动     左侧视图已激活

我正在寻找一个抽象或一组抽象来提供这个 功能干净。到目前为止,这是我的思考过程:

起初我以为我有一个Monoid,身份是空树,而且 mappend只会将另一个分支附加到树上,但这不起作用 因为我有两个操作:垂直追加和水平追加和操作 当他们混在一起时,他们没有联系。

接下来我想:“我的一些操作取决于他们的背景'所以我可能有一个Comonad。树的版本 我没有作为合作伙伴工作,因为我在分支上没有extract的值,所以我重组了我的树 像这样:

data Tree a = Node Direction [Tree a] a [Tree a]
    deriving (Functor)

但这仍然是 没有处理“分裂”的情况。一个节点基于其中的内容,这与签名(View -> Tree View) -> Tree View -> Tree View匹配,签名与Monad的bind统一,所以也许我有一个monad?我可以实现monad 原始的树定义,但不能为我的Comonad树版本找出它。

有没有办法可以在这里获得两全其美?我是用Comonad / Monad挖出错误的树吗? 基本上我正在寻找一种优雅的方法来在我的数据结构上建模这些函数。谢谢!

如果您想查看完整代码,则函数为here,当前树为here

2 个答案:

答案 0 :(得分:6)

我放弃了试图把它塞进评论中。 Conor McBride有一个完整的talk,而Sam Lindley是paper的一大块,所有关于使用monads来雕刻2D空间。既然你要求一个优雅的解决方案,我觉得有必要给你一个关于他们工作的盆栽总结,虽然我不一定建议把它构建到你的代码库中 - 我怀疑它可能只是简单地使用一个库与boxes类似,并通过手动错误处理手动切割和调整逻辑大小。

您的第一个Tree是朝着正确方向迈出的一步。我们可以编写一个Monad实例来将树移植到一起:

instance Monad Tree where
    return = Leaf
    Leaf x >>= f = f x
    Branch d l r >>= f = Branch d (l >>= f) (r >>= f)

Tree's join树上有一棵树,叶子可以让你一直走到山脚,而不会停下来一半的呼吸。将Tree视为free monad可能会有所帮助,正如@danidiaz在an answer中所示。或Kmett might say您使用非常简单的语法,允许将Var称为Leaf的术语替换。

无论如何,重点是你可以通过逐渐切割树叶来使用>>=种植树木。在这里,我有一个一维的UI(暂时忘记Direction),只有一个包含String的窗口,并且通过反复将其切成两半我最终得到八个较小的UI窗户。

halve :: [a] -> Tree [a]
halve xs = let (l, r) = splitAt (length xs `div` 2) xs
         in Node (Leaf l) (Leaf r)

ghci> let myT = Leaf "completeshambles"
-- |completeshambles|
ghci> myT >>= halve
Node (Leaf "complete") (Leaf "shambles")
-- |complete|shambles|
ghci> myT >>= halve >>= halve
Node (Node (Leaf "comp") (Leaf "lete")) (Node (Leaf "sham") (Leaf "bles"))
-- |comp|lete|sham|bles|
ghci> myT >>= halve >>= halve >>= halve
Node (Node (Node (Leaf "co") (Leaf "mp")) (Node (Leaf "le") (Leaf "te"))) (Node (Node (Leaf "sh") (Leaf "am")) (Node (Leaf "bl") (Leaf "es")))
-- |co|mp|le|te|sh|am|bl|es|

(在现实生活中,你可能只是一次切断一个窗口,通过检查你的绑定功能中的ID并保持不变,如果它不是你正在寻找的那个为。)

问题在于,Tree并不了解物理空间是有限且宝贵的资源这一事实。 fmap允许您将a替换为b,但如果b占用的空间超过a,则生成的结构将无法适应屏幕{1}}做了!

ghci> fmap ("in" ++) myT
Leaf "incompleteshambles"

这在两个方面变得更加严重,因为盒子可以相互推动并撕裂。如果中间窗口被意外调整大小,我会得到一个畸形框或中间的一个洞(取决于它在树中的位置)。

+-+-+-+         +-+-+-+            +-+-+  +-+
| | | |         | | | |            | | |  | |
+-+-+-+         +-+-+-++-+   or,   +-+-+--+-+
| | | |  ---->  | |    | | perhaps | |    | |
+-+-+-+         +-+-+-++-+         +-+-+--+-+
| | | |         | | | |            | | |  | |
+-+-+-+         +-+-+-+            +-+-+  +-+

扩展窗口是一件非常合理的事情,但在现实世界中,它扩展到的空间必须来自某个地方。你不能在不缩小另一个窗口的情况下生长一个窗口,反之亦然。这不是可以使用>>=执行的操作,>>=在各个叶节点执行本地替换;你需要看一个窗口的兄弟姐妹,知道谁占用了它旁边的空间。

因此,您不应该被允许使用data Nat = Z | S Nat type family n :+ m where Z :+ m = m S n :+ m = S (n :+ m) 来调整此类内容的大小。 Lindley和McBride的想法是教类型检查器如何将盒子拼接在一起。使用类型级自然数和加法,

a, Box a :: (Nat, Nat) -> *
-- so Box :: ((Nat, Nat) -> *) -> (Nat, Nat) -> *

它们使用由宽度和高度索引的内容。 (在论文中,他们使用表示为矢量矢量的2D矩阵,但为了提高效率,您可能需要使用一个测量其大小的幻像类型的数组。)

Hor

使用Ver并排放置两个盒子要求它们具有相同的高度,并且使用data Box a wh where Content :: a '(w, h) -> Box a '(w, h) Hor :: Box a '(w1, h) -> Box a '(w2, h) -> Box a '(w1 :+ w2, h) Ver :: Box a '(w, h1) -> Box a '(w, h2) -> Box a '(w, h1 :+ h2) 将它们放在彼此之上需要它们具有相同的宽度。

return

现在我们已经准备好建造一个monad来将这些树嫁接在一起。 Box的语义没有改变 - 它将一个2D对象放在return :: a wh -> Box a wh return = Content 中。

>>=

现在让我们考虑Content。通常,盒子由许多不同大小的Hor (Ver (Content 2x1) (Content 2x2)) Content 1x3组成,以某种方式组成以产生更大的盒子。下面我有三个内容大小为2x1,2x2和1x3组成一个3x3盒子。此框看起来像 2x1 +--+-+ | | | +--+ |1x3 | | | | | | +--+-+ 2x2

>>=

当您>>=的来电者知道您的包装盒的外部尺寸时,您不知道构成它的各个内容的尺寸。如何使用>>=剪切内容时,如何保留内容的大小?您必须编写一个保留大小的函数,而不用先验知道大小是什么。

因此Box获取已知大小wh的{​​{1}},将其拆分以查找内容,使用保留内容(未知)大小的函数对其进行处理给它*,并将它重新组合在一起以生成一个具有相同大小wh的新盒子。注意rank-2类型,反映了>>=的调用者无法控制将继续调用内容的维度的事实。

(>>=) :: Box a wh -> (forall wh2. a wh2 -> Box b wh2) -> Box b wh
Content x >>= f = f x
Hor l r >>= f = Hor (l >>= f) (r >>= f)
Ver t b >>= f = Ver (t >>= f) (b >>= f)

如果您使用类型同义词~>来保存索引并翻转参数,那么对于常规=<<,您会得到类似于Monad的内容,但是会有不同的类型箭头Kleisli的作品也很漂亮。

type a ~> b = forall x. a x -> b x

return :: a ~> Box a
(=<<) :: (a ~> Box b) -> (Box a ~> Box b)
(>=>) :: (a ~> Box b) -> (b ~> Box c) -> (a ~> Box c)

所以这个monad超过了索引集。 (Kleisli Arrows of Outrageous Fortune中的更多信息。)在the paper中,他们构建了更多基础设施来支持裁剪和重新排列框,这可能对您构建UI很有用。为了提高效率,您可能还决定使用zipper跟踪当前关注的窗口,这是一项有趣的练习。顺便说一句,我认为 Hasochism 是对一般花式类型的一个很好的介绍,而不仅仅是这个特定问题的解决方案。

*假设a的索引确实是其物理尺寸的准确度量

答案 1 :(得分:1)

我会将您的类型表示为monad并使用>>=来处理split

{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Free

data Direction = Hor | Vert

data TreeF a = TreeF Direction a a deriving Functor

type Tree a = Free TreeF a

对于close,我可能会使用递归方案中的catapara,因为close似乎可以自下而上并且最多需要节点父母和兄弟姐妹的知识。您也可以转向Control.Lens.Plated

顺便说一下,Free已经有Recursive个实例。 FreeF TreeF a将是相应的代数。但你提到它并没有很好地发挥作用。

直接使用FreeFreeT构造函数可能会很麻烦。也许一些模式同义词可以帮助那里。