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“字符串移动”函数的主要内容,如果未能解决你的问题,请参考以下文章

作为函数输入的字符串的Haskell函数头

Haskell - 按整数值对带有字符串的列表进行排序的函数

基于 Haskell 中的字符串映射证明打印函数的穷举性

简单的输入函数Haskell

Haskell 示例中的函数组合

Haskell 函数将 Int 转换为 alpha numerotation [关闭]