繁体   English   中英

在 Haskell 中将字符串拆分为类型

[英]Splitting string into type in Haskell

我需要创建一个解析函数。 我是 Haskell 的新手,我很感兴趣可以仅使用 GHC 基本函数在 Haskell 中实现我的想法。

所以问题是:我在字符串中有这样的消息,其坐标和值类似于 (x: 01, 01, ... y:01, 02,: v: X, Y, Z) 并且我需要像 ( [字符]、[整数]、[整数])。

在像 C 这样的语言中,我会创建循环并从头开始,然后检查然后将其放入数组中,但我担心这在 Haskell 中不起作用。 有人可以就这个问题的平易近人的解决方案给出提示吗?

如果您习惯于使用循环进行命令式编程,您实际上可以使用直接递归对 Haskell 的命令式解决方案进行相当直接的翻译。

请记住,这不是获得可行解决方案的最简单最佳方法,但学习该技术是件好事,这样您就可以了解哪些更惯用的解决方案正在为您抽象。

基本原理是用递归函数替换每个循环,并用该函数的累加器参数替换每个可变变量。 在这里您将在循环的迭代修改变量,只是做一个新的变量; 在循环迭代之间修改它的地方,使用不同的参数代替该参数调用循环函数。

举一个简单的例子,考虑计算一个整数列表的总和。 在 C 中,可以这样写:

struct ListInt { int head; struct ListInt *tail; }

int total(ListInt const *list) {
    int acc = 0;
    ListInt const *xs = list;
    while (xs != NULL) {
        acc += xs->head;
        xs = xs->tail;
    }
    return acc;
}

我们可以将其从字面上翻译为低级 Haskell:

total :: [Int] -> Int
total list
  = loop
    0     -- acc = 0
    list  -- xs = list

  where

    loop
      :: Int    -- int acc;
      -> [Int]  -- ListInt const *xs;
      -> Int

    loop acc xs                 -- loop:

      | not (null xs) = let     -- if (xs != NULL) {
        acc' = acc + head xs    --   acc += xs->head;
        xs' = tail xs           --   xs = xs->tail;
        in loop acc' xs'        --   goto loop;
                                -- } else {
      | otherwise = acc         --   return acc;
                                -- }

外部函数total设置初始状态,内部函数loop处理输入的迭代。 在这种情况下, total在循环后立即返回,但如果在循环后还有更多代码来处理结果,则total

total list = let
  result = loop 0 list
  in someAdditionalProcessing result

在 Haskell 中,辅助函数通过将结果列表添加到累加器列表的开头来累积结果列表是非常常见的: ,然后在循环后反转此列表,因为将值附加到列表的末尾要多得多昂贵。 您可以将此模式视为使用列表作为堆栈,其中:是“推送”操作。

此外,我们可以立即进行一些简单的改进。 首先,如果我们的代码错误并且我们在空列表上调用它们,访问器函数headtail可能会抛出错误,就像访问NULL指针的headtail成员一样(尽管异常比段错误更清晰!),所以我们可以简化它让它更安全地使用模式匹配,而不是警卫和head / tail

loop :: Int -> [Int] -> Int
loop acc [] = acc
loop acc (h : t) = loop (acc + h) t

最后,这种递归模式恰好是折叠:有一个累加器的初始值,为输入的每个元素更新,没有复杂的递归。 所以整个事情都可以用foldl'来表达:

total :: [Int] -> Int
total list = foldl' (\ acc h -> acc + h) 0 list

然后缩写:

total = foldl' (+) 0

所以,为了解析你的格式,你可以遵循类似的方法:你有一个字符列表而不是整数列表,而不是单个整数结果,你有一个复合数据类型,但整体结构非常相似:

parse :: String -> ([Char], [Int], [Int])
parse input = let
  (…, …, …) = loop ([], [], []) input
  in …

  where
    loop (…, …, …) (c : rest) = …  -- What to do for each character.
    loop (…, …, …) []         = …  -- What to do at end of input.

如果有不同的子解析器,您将在命令式语言中使用状态机,则可以使累加器包含不同状态的数据类型。 例如,这是一个用空格分隔的数字的解析器:

import Data.Char (isSpace, isDigit)

data ParseState
  = Space
  | Number [Char]  -- Digit accumulator

numbers :: String -> [Int]
numbers input = loop (Space, []) input
  where

    loop :: (ParseState, [Int]) -> [Char] -> [Int]

    loop (Space, acc) (c : rest)
      | isSpace c = loop (Space, acc) rest       -- Ignore space.
      | isDigit c = loop (Number [c], acc) rest  -- Push digit.
      | otherwise = error "expected space or digit"

    loop (Number ds, acc) (c : rest)
      | isDigit c = loop (Number (c : ds), acc) rest  -- Push digit.
      | otherwise
        = loop
          (Space, read (reverse ds) : acc)  -- Save number, expect space.
          (c : rest)                        -- Repeat loop for same char.

    loop (Number ds, acc) [] = let
      acc' = read (reverse ds) : acc  -- Save final number.
      in reverse acc'                 -- Return final result.

    loop (Space, acc) [] = reverse acc  -- Return final result.

当然,如您所知,这种方法很快就会变得非常复杂! 即使你的代码写得很紧凑,或者用折叠来表达,如果你在单个字符和解析器状态机的层面上工作,也需要大量的代码来表达你的意思,并且有很多机会错误。 更好的方法是考虑此处工作的数据流,并将解析器与高级组件放在一起。

例如,上述解析器的意图是执行以下操作:

  • 在空白处拆分输入

  • 对于每个拆分,将其读取为整数

这可以用wordsmap函数非常直接地表达:

numbers :: String -> [Int]
numbers input = map read (words input)

一行可读而不是几十行! 显然这种方法更好。 考虑如何以这种方式表达您尝试解析的格式。 如果你想避免像split这样的库,你仍然可以编写一个函数来使用breakspantakeWhilebase函数在分隔符上拆分字符串; 然后您可以使用它将输入拆分为记录,并将每个记录拆分为字段,并相应地将字段解析为整数或文本名称。

但是在 Haskell 中解析的首选方法根本不是手动拆分输入,而是使用解析器组合器库,如megaparsec Text.ParserCombinators.ReadP下的base也有解析器组合Text.ParserCombinators.ReadP 有了这些,你可以抽象地表达一个解析器,根本不用讨论拆分输入,只需将子解析器与标准接口( FunctorApplicativeAlternativeMonad )结合起来,例如:

import Data.Char (isDigit)

import Text.ParserCombinators.ReadP
  ( endBy
  , eof
  , munch1
  , readP_to_S
  , skipSpaces
  , skipSpaces
  )

numbers :: String -> [Int]
numbers = fst . head . readP_to_S onlyNumbersP
  where
    onlyNumbersP :: ReadP [Int]
    onlyNumbersP = skipSpaces *> numbersP <* eof

    numbersP :: ReadP [Int]
    numbersP = numberP `endBy` skipSpaces

    numberP :: ReadP Int
    numberP = read <$> munch1 isDigit

这是我在您的情况下推荐的方法。 解析器组合器也是在实践中习惯使用 applicatives 和 monad 的好方法。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM