[英]Purely functional data structures for text editors
什么是文本编辑器的纯功能数据结构? 我希望能够在文本中插入单个字符并以可接受的效率从文本中删除单个字符,并且我希望能够保留旧版本,因此我可以轻松地撤消更改。
我应该只使用字符串列表并重用不同版本的行吗?
我不知道这个建议对于“好”的复杂定义是否“好”,但它很容易和有趣。 我经常设置一个练习来编写Haskell中文本编辑器的核心,并链接我提供的渲染代码。 数据模型如下。
首先,我定义了x
-elements列表中的游标是什么,其中游标上的可用信息具有某种类型m
。 ( x
将变成Char
或String
。)
type Cursor x m = (Bwd x, m, [x])
这个Bwd
东西只是落后的“snoc-lists”。 我想保持强烈的空间直觉,所以我在我的代码中扭转局面,而不是在我脑海中。 这个想法是最靠近光标的东西是最容易访问的。 这就是拉链的精神。
data Bwd x = B0 | Bwd x :< x deriving (Show, Eq)
我提供了一个免费的单例类型来充当游标的可读标记......
data Here = Here deriving Show
...因此,我可以说在String
某个地方是什么
type StringCursor = Cursor Char Here
现在,为了表示多行的缓冲区,我们需要使用光标在行的上方和下方的String
,以及中间的StringCursor
,用于我们当前正在编辑的行。
type TextCursor = Cursor String StringCursor
这个TextCursor
类型是我用来表示编辑缓冲区的状态。 这是一个两层拉链。 我向学生提供了代码,用于在启用ANSI-escape的shell窗口中渲染文本的视口,确保视口包含光标。 他们所要做的就是实现更新TextCursor
以响应击键的代码。
handleKey :: Key -> TextCursor -> Maybe (Damage, TextCursor)
其中handleKey
应该返回Nothing
如果按键是没有意义的,但在其他方面提供Just
更新TextCursor
和“伤害报告”,后者是一个
data Damage
= NoChange -- use this if nothing at all happened
| PointChanged -- use this if you moved the cursor but kept the text
| LineChanged -- use this if you changed text only on the current line
| LotsChanged -- use this if you changed text off the current line
deriving (Show, Eq, Ord)
(如果你想知道返回Nothing
和返回Just (NoChange, ...)
之间的区别是Nothing
,请考虑是否还希望编辑器发出蜂鸣声。)损坏报告告诉渲染器需要做多少工作才能完成使显示的图像更新。
Key
类型只为可能的击键提供可读的dataype表示,从原始的ANSI转义序列中抽象出来。 它并不起眼。
通过提供这些工具包,我为学生们提供了一个关于上下这个数据模型的大线索:
deactivate :: Cursor x Here -> (Int, [x])
deactivate c = outward 0 c where
outward i (B0, Here, xs) = (i, xs)
outward i (xz :< x, Here, xs) = outward (i + 1) (xz, Here, x : xs)
deactivate
函数用于将焦点移出Cursor
,给你一个普通的列表,但告诉你光标在哪里。 相应的activate
函数尝试将光标放在列表中的给定位置:
activate :: (Int, [x]) -> Cursor x Here
activate (i, xs) = inward i (B0, Here, xs) where
inward _ c@(_, Here, []) = c -- we can go no further
inward 0 c = c -- we should go no further
inward i (xz, Here, x : xs) = inward (i - 1) (xz :< x, Here, xs) -- and on!
我向学生提供了一个故意错误和不完整的handleKey
定义
handleKey :: Key -> TextCursor -> Maybe (Damage, TextCursor)
handleKey (CharKey c) (sz,
(cz, Here, cs),
ss)
= Just (LineChanged, (sz,
(cz, Here, c : cs),
ss))
handleKey _ _ = Nothing
它只处理普通的字符击键,但使文本向后出现。 人们很容易看到的字符c
正好出现的Here
。 我邀请他们修复错误并添加箭头键,退格键,删除键,返回键等功能。
它可能不是最有效的表示,但它纯粹是功能性的,并使代码能够具体地符合我们关于正在编辑的文本的空间直觉。
Vector[Vector[Char]]
可能是一个不错的选择。 它是一个IndexedSeq
因此具有不错的更新/前置/更新性能,与您提到的List
不同。 如果你看一下Performance Characteristics ,它是唯一提到的具有有效恒定时间更新的不可变集合。
我们在Yi中使用了一个文本拉链,这是Haskell中一个严肃的文本编辑器实现。
下面描述了不可变状态类型的实现,
http://publications.lib.chalmers.se/records/fulltext/local_94979.pdf
http://publications.lib.chalmers.se/records/fulltext/local_72549.pdf
和其他论文。
我建议将拉链与Data.Sequence.Seq结合使用,它基于手指树 。 所以你可以把当前状态表示为
data Cursor = Cursor { upLines :: Seq Line
, curLine :: CurLine
, downLines :: Seq Line }
这使得向上/向下移动光标的复杂性为O(1) ,并且由于splitAt
和(><)
(并集)具有O(log(min(n1,n2)))复杂度,因此您将得到O (log(L))向上/向下跳过L行的复杂性。
您可以为CurLine
提供类似的拉链结构,以便在光标之前,之后和之后保留一系列字符。
Line
可以节省空间,例如ByteString 。
为此,我为我的vty-ui
库实现了一个拉链。 你可以看看这里:
https://github.com/jtdaugherty/vty-ui/blob/master/src/Graphics/Vty/Widgets/TextZipper.hs
Clojure社区正在研究RRB Trees (Relaxed Radix Balanced)作为数据向量的持久数据结构,可以有效地连接/切片/插入等。
它允许在O(log N)时间内连接,插入索引和拆分操作。
我想一个专门用于字符数据的RRB树非常适合大型“可编辑”文本数据结构。
想到的可能性是:
带有数字索引的“Text”类型。 它将文本保存在缓冲区的链表中(内部表示为UTF16),因此理论上它的计算复杂度通常是链表(例如索引是O(n)),实际上它比传统链接快得多除非您将整个Wikipedia存储在缓冲区中,否则您可能会忘记n的影响。 尝试对100万个字符文本进行一些实验,看看我是否正确(我实际上没有做过什么,BTW)。
文本拉链:将光标后的文本存储在一个文本元素中,将光标前的文本存储在另一个文本元素中。 将光标传输文本从一侧移动到另一侧。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.