用于文本编辑器的纯功能数据结构

时间:2012-09-10 19:19:24

标签: scala haskell data-structures functional-programming text-editor

文本编辑器的纯功能数据结构是什么?我希望能够在文本中插入单个字符并以可接受的效率从文本中删除单个字符,并且我希望能够保留旧版本,因此我可以轻松撤消更改。

我应该只使用字符串列表并重复使用不会因版本而异的行吗?

8 个答案:

答案 0 :(得分:54)

我不知道这个建议对于“好”的复杂定义是否“好”,但它很容易和有趣。我经常设置一个练习来编写Haskell中文本编辑器的核心,并链接我提供的渲染代码。数据模型如下。

首先,我定义了x - 元素列表中的游标是什么,其中游标上的可用信息具有某种类型m。 (x将结果为CharString。)

type Cursor x m = (Bwd x, m, [x])

这个Bwd只是落后的“snoc-lists”。我想保持强烈的空间直觉,所以我在我的代码中扭转局面,而不是在我脑海中。这个想法是最靠近光标的东西是最容易访问的。这就是The Zipper的精神。

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和“损坏报告”,后者是< / p>

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, ...)之间有什么区别,请考虑是否还希望编辑器发出哔声。)损坏报告告诉渲染器它有多少工作量需要做的是使显示的图像更新。

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出现正确。{{1}}。我邀请他们修复错误并添加箭头键,退格键,删除键,返回键等功能。

它可能不是最有效的表示,但它纯粹是功能性的,并使代码能够具体地符合我们对正在编辑的文本的空间直觉。

答案 1 :(得分:10)

Vector[Vector[Char]]可能是一个不错的选择。它是IndexedSeq所以具有不错的更新/前置/更新性能,与您提到的List不同。如果你看一下Performance Characteristics,它是唯一提到的有效恒定时间更新的不可变集合。

答案 2 :(得分:7)

我们在Yi中使用了一个文本拉链,这是Haskell中一个严肃的文本编辑器实现。

下面描述了不可变状态类型的实现,

http://publications.lib.chalmers.se/records/fulltext/local_94979.pdf

http://publications.lib.chalmers.se/records/fulltext/local_72549.pdf

等论文。

答案 3 :(得分:5)

答案 4 :(得分:4)

我建议将zippers与基于Data.Sequence.Seqfinger trees结合使用。所以你可以把当前状态表示为

data Cursor = Cursor { upLines :: Seq Line
                     , curLine :: CurLine
                     , downLines :: Seq Line }

这为您提供了 O(1)复杂度,可以将光标向上/向下移动一行,并且由于splitAt(><)(union)同时具有 O (log(min(n1,n2)))复杂度,你将获得 O(log(L))复杂性,用于向上/向下跳过 L

CurLine可以使用类似的拉链结构,以便在光标之前,之后和之后保留一系列字符。

Line可以节省空间,例如ByteString

答案 5 :(得分:4)

我为vty-ui库实现了拉链。你可以看看这里:

https://github.com/jtdaugherty/vty-ui/blob/master/src/Graphics/Vty/Widgets/TextZipper.hs

答案 6 :(得分:2)

Clojure社区正在研究RRB Trees(宽松基数平衡)作为数据向量的持久数据结构,可以有效地连接/切片/插入等。

它允许在O(log N)时间内连接,插入索引和拆分操作。

我认为专门用于字符数据的RRB树非常适合大型“可编辑”文本数据结构。

答案 7 :(得分:1)

想到的可能性是:

  1. 带有数字索引的“文本”类型。它将文本保存在缓冲区的链表中(内部表示为UTF16),因此理论上它的计算复杂度通常是链表(例如索引是O(n)),实际上它比传统链接快得多除非您将整个Wikipedia存储在缓冲区中,否则您可能会忘记n的影响。尝试对100万个字符文本进行一些实验,看看我是否正确(我实际上没有做过,BTW)。

  2. 文本拉链:将光标后的文本存储在一个文本元素中,将之前的文本存储在另一个文本元素中。将光标传输文本从一侧移动到另一侧。