Haskell“字符串移动”函数
Posted
技术标签:
【中文标题】Haskell“字符串移动”函数【英文标题】:Haskell "string movement" functions 【发布时间】:2015-12-30 10:21:54 【问题描述】:我正在尝试使用 Haskell、hscurses 和 Data.Text 构建一个简单的文本编辑器。我对 Haskell 很陌生。
这是我的代码中的一个 sn-p:
data Cursor = Cursor
position :: Int,
line :: Int,
column :: Int
deriving (Eq, Show)
isNewline :: Char -> Bool
isNewline c = c == '\n'
onNewline :: T.Text -> Int -> Bool
onNewline buf pos
| pos >= T.length buf = False
| otherwise = isNewline $ T.index buf pos
findIndex :: (Char -> Bool) -> T.Text -> Int -> Maybe Int
findIndex pred buf pos
| buf == T.empty = Just 0
| otherwise = rightWhile pos
where rightWhile pos
| pos > bufMax buf = Nothing
| pred $ T.index buf pos = Just pos
| otherwise = rightWhile (pos + 1)
findIndexLeft :: (Char -> Bool) -> T.Text -> Int -> Maybe Int
findIndexLeft pred buf pos = leftWhile pos
where leftWhile pos
| pos < 0 = Nothing
| pred $ T.index buf pos = Just pos
| otherwise = leftWhile (pos - 1)
startOfLine :: T.Text -> Int -> Int
startOfLine buf pos = case findIndexLeft isNewline buf (pos - 1) of
Nothing -> 0
Just p -> p + 1
endOfLine :: T.Text -> Int -> Int
endOfLine buf pos = case findIndex isNewline buf pos of
Nothing -> 1 + bufMax buf
Just p -> p
lineOffset :: T.Text -> Int -> Int
lineOffset buf pos = pos - startOfLine buf pos
lineLength :: T.Text -> Int -> Int
lineLength buf pos = endOfLine buf pos - startOfLine buf pos
bufMax :: T.Text -> Int
bufMax buf = max 0 $ T.length buf - 1
bufLines :: T.Text -> Int
bufLines = T.foldl (\acc c -> if isNewline c then (acc+1) else acc) 0
moveCursorRight :: T.Text -> Cursor -> Cursor
moveCursorRight buf c@(Cursor pos line col)
| buf == T.empty = c
| otherwise = Cursor newPos newLine newCol
where end = 1 + bufMax buf
onEnd = pos == end
newPos = clip (pos + 1) 0 end
newLine = if onNewline buf pos && not onEnd
then line + 1
else line
newCol = lineOffset buf newPos
moveCursorLeft :: T.Text -> Cursor -> Cursor
moveCursorLeft buf (Cursor pos line col) =
Cursor newPos newLine newCol
where onStart = pos == 0
newPos = clipLow (pos - 1) 0
newLine = if onNewline buf newPos && not onStart
then line - 1
else line
newCol = lineOffset buf newPos
-- More movement functions follow...
此代码的问题在于,对于数千行长的缓冲区,它变得非常慢。这可能是因为使用了索引函数,它是 O(n),而不是像在 C 中那样的恒定时间。
经验丰富的 Haskeller 会如何处理这个问题?在 Haskell 中实现字符串“移动”的合理有效方法是什么?移动也应该是可组合的,即我希望能够实现“向下移动一行”等方面的“Page down”。
编辑:更新
如果有人需要这个,这就是我最终得到的。
type Line = T.Text
data BufferContext = BufferContext
before :: [Line],
at :: Line,
after :: [Line]
deriving (Eq, Show)
moveCursorRight :: Cursor -> Cursor
moveCursorRight c@(Cursor pos line col bc@(BufferContext before at after))
| col >= T.length at = moveCursorDown c
| otherwise = Cursor (pos+1) line (col+1) bc
moveCursorLeft :: Cursor -> Cursor
moveCursorLeft c@(Cursor pos line col bc@(BufferContext before at after))
| col <= 0 = upCursor column = if null before then 0 else T.length $ head before
| otherwise = Cursor (pos-1) line (col-1) bc
where upCursor = moveCursorUp c
moveCursorDown :: Cursor -> Cursor
moveCursorDown c@(Cursor _ _ _ (BufferContext _ _ [])) = c
moveCursorDown c@(Cursor _ cLine _ (BufferContext before at (l:ls))) =
c line = cLine+1,
column = 0,
context = BufferContext (at:before) l ls
moveCursorUp c@(Cursor _ _ _ (BufferContext [] _ _)) = c
moveCursorUp c@(Cursor _ cLine _ (BufferContext (l:ls) at after)) =
c line = cLine-1,
column = 0,
context = BufferContext ls l (at:after)
这个实现在 100 万行上非常有用,这对我来说已经足够了。但是,这种方法仍然存在一个问题。如果我想跳到一条随机线,我必须一个接一个地移动,这可能会很慢。但是,这仍然是对原始方法的巨大改进。
我也尝试过将上下文实现为
data BufferContext = BufferContext
before :: T.Text,
at :: Char,
after :: T.Text
deriving (Eq, Show)
但这并没有太大帮助,因为“at”必须与“before”一致,根据文档,T.cons
是 O(n)...此外,以行为中心的方法在以下情况下更好实际显示完成。
感谢所有帮助过的人!
【问题讨论】:
您可能希望将缓冲区表示为行列表,使用Zipper 表示您当前的位置。对于 Agda 中的指导示例(您可以在 Haskell 中复制相当多的工作),您可以查看 this exercise 另见:***.com/questions/4046246/…。有一种方法可以将findIndex
加速到 O(n),但之后您仍然使用索引位置,因此从长远来看它并不能真正帮助您。
如果你需要跳来跳去,你应该使用Data.Sequence
从一个list拉链切换到一个sequence拉链。 data Zipper a = Zipper (Seq a) a (Seq a)
或类似的。与列表拉链不同,您可以保持一切井井有条,因为结束与开始一样快。并且您可以在O(log n)
时间移动n
步骤。
【参考方案1】:
正如gallais 在 cmets 中所说,您想使用拉链。这个想法是您的“光标”实际上是这样的数据结构:
type Line = T.Text
data TextZipper = TextZipper
textBefore :: [Line],
currentLine :: Line,
textAfter :: [Line]
诀窍在于“textBefore”包含光标上方的行以相反的顺序。因此,要向下移动一行,请将“currentLine”放在“textBefore”的开头,并从“textAfter”中取出新的“currentLine”,如下所示:
moveDown :: TextZipper -> Maybe TextZipper
moveDown tzip = case textAfter tzip of
[] -> Nothing -- Already at the bottom of the file.
t:ts -> TextZipper
textBefore = currentLine tzip : textBefore tzip,
currentLine = t,
textAfter = ts
moveUp 将非常相似。您还需要一个 textZipperToList 函数来提取拉链的内容以进行保存,还需要一个 textZipperFromList 函数。
我记得在某处读到 Emacs 使用了类似的概念,只是它是按字符而不是按行来实现的。缓冲区表示为两个文本块,一个是光标之前的块,另一个是光标之后的块。移动光标是通过将字符从一个块复制到另一个块来完成的。这里的概念相同。鉴于此,您可能需要考虑将每个行列表替换为单个 Text 值。
【讨论】:
以上是关于Haskell“字符串移动”函数的主要内容,如果未能解决你的问题,请参考以下文章