我放弃了尝试将其塞进评论中。Conor McBride与 Sam Lindley进行了一场完整的演讲,以及一大篇论文,都是关于使用 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
'sjoin
在其叶子上带有树木的树木,让您一路走到底部,而不必停下来呼吸一半。正如@danidiaz 在答案Tree
中所显示的那样,将其视为免费的 monad可能会有所帮助。或者Kmett 可能会说您有一个非常简单的语法允许术语替换,其被称为.Var
Leaf
无论如何,重点是您可以>>=
通过逐步砍伐树叶来种植树木。在这里,我有一个一维 UI(让我们Direction
暂时忘掉),其中包含一个包含 a 的窗口String
,通过反复将其切成两半,我最终得到了八个较小的窗口。
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
s替换为 s,但如果s 占用的空间比s 多b
,则生成的结构将不适合屏幕!b
a
ghci> fmap ("in" ++) myT
Leaf "incompleteshambles"
这在二维上变得更加严重,因为盒子可以相互推动并撕裂。如果中间的窗口被意外调整大小,我要么得到一个畸形的盒子,要么在中间有一个洞(取决于它在树上的位置)。
+-+-+-+ +-+-+-+ +-+-+ +-+
| | | | | | | | | | | | |
+-+-+-+ +-+-+-++-+ or, +-+-+--+-+
| | | | ----> | | | | perhaps | | | |
+-+-+-+ +-+-+-++-+ +-+-+--+-+
| | | | | | | | | | | | |
+-+-+-+ +-+-+-+ +-+-+ +-+
扩展一个窗口是一件非常合理的事情,但在现实世界中,它扩展的空间必须来自某个地方。你不能在不缩小另一个窗口的情况下增长一个窗口,反之亦然。这不是可以用 来完成的那种操作>>=
,它在单个叶节点上执行局部替换;您需要查看窗口的兄弟姐妹才能知道谁占用了它附近的空间。
所以你不应该被允许用来>>=
调整这样的内容。Lindley 和 McBride 的想法是教类型检查器如何将盒子拼接在一起。使用类型级自然数和加法,
data Nat = Z | S Nat
type family n :+ m where
Z :+ m = m
S n :+ m = S (n :+ m)
它们处理按宽度和高度索引的内容。(在论文中,他们使用表示为向量向量的 2D 矩阵,但为了提高效率,您可能希望使用具有幻像类型的数组来测量其大小。)
a, Box a :: (Nat, Nat) -> *
-- so Box :: ((Nat, Nat) -> *) -> (Nat, Nat) -> *
将两个盒子并排放置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
没有改变——它把一个 2D 对象Box
放在自己的 a 中。
return :: a wh -> Box a wh
return = Content
现在让我们考虑一下>>=
。一般来说,一个盒子是由许多Content
不同大小的碎片组成的,它们以某种方式组合成一个更大的盒子。下面我有三个大小为 2x1、2x2 和 1x3 的内容组成一个 3x3 的盒子。这个盒子看起来像Hor (Ver (Content 2x1) (Content 2x2)) Content 1x3
.
2x1
+--+-+
| | |
+--+ |1x3
| | |
| | |
+--+-+
2x2
虽然您( 的调用者>>=
)知道您的盒子的外部尺寸,但您不知道组成它的各个内容的尺寸。当您使用 切割内容时,您如何期望保留内容的大小>>=
?您必须编写一个函数来保留大小,而无需先验知道大小是什么。
因此,>>=
需要Box
一个已知大小的 a wh
,将其拆开以查找内容,使用保留您给它的内容的(未知)大小的函数对其进行处理*,然后将其重新组合在一起以生成具有相同大小的新框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
s 相似的东西,但带有不同类型的箭头。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)
这就是索引集上的单子。(更多内容见Kleisli Arrows of Outrageous Fortune。)在论文中,他们构建了更多基础设施来支持裁剪和重新排列框,这可能对您构建 UI 很有用。为了提高效率,您可能还决定使用zipper跟踪当前聚焦的窗口,这是一个有趣的练习。顺便说一句,我认为Hasochism是对一般花式类型的一个很好的介绍,而不仅仅是解决这个特定问题。
*假设a
的指数确实是其物理尺寸的准确度量