【问题标题】:Splitting string into type in Haskell在 Haskell 中将字符串拆分为类型
【发布时间】:2020-09-20 18:33:16
【问题描述】:

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

所以问题是:我在字符串中有这样的消息,其坐标和值如 (x: 01, 01, ... y:01, 02,: v: X, Y, Z) 我需要像 ([Char], [Int], [Int]) 来解析它的类型。

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

【问题讨论】:

  • 我不相信这种类型是个好主意,因为这意味着Chars 列表的长度可能与Ints 列表的长度不同。使用[(Char, Int, Int)] 可能会更好,或者如果每个字符的数字列表可能不同,[(Char, [Int])]
  • 您可以从将字符串分解为其组件开始。可能使用类似break 甚至words 之类的东西——如果没有确切的语法就很难分辨。然后,您可以使用模式匹配或takeWhile 和朋友编写递归解析器。
  • 消息的语法写在 bencode 中,像这样 "l2:vsl1:X1:O1:X1:O1:Xe2:ysl1:01:01:11:11:2e2:xsl1: 01:21:01:21:0ee”。
  • 解析可以试试parsec, megaparsec or attoparsec

标签: haskell


【解决方案1】:

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

请记住,这不是获得有效解决方案的最简单最佳方法,但最好学习该技术,以便您了解更多内容惯用的解决方案正在为您抽象。

基本原则是将每个循环替换为递归函数,并将每个可变变量替换为该函数的累加器参数。在循环的迭代中修改变量 within 时,只需创建一个新变量;在次循环迭代之间修改它的地方,用不同的参数代替该参数调用循环函数。

举个简单的例子,考虑计算一个整数列表的总和。在 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 中,帮助函数通过将结果列表添加到带有 : 的累加器列表的开头 来累积结果列表是非常常见的,然后在循环后反转该列表,因为将值附加到列表的 end 成本要高得多。您可以将此模式视为将列表用作堆栈,其中: 是“推送”操作。

此外,我们可以立即进行一些简单的改进。首先,访问器函数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 之类的库,您仍然可以使用 base 函数(例如 breakspantakeWhile)编写一个函数来拆分分隔符上的字符串;然后您可以使用它将输入拆分为记录,并将每个记录拆分为字段,并相应地将字段解析为整数或文本名称。

但在 Haskell 中解析的首选方法根本不是手动拆分输入,而是使用像 megaparsec 这样的 解析器组合器 库。在base 中也有解析器组合器,在Text.ParserCombinators.ReadP 下。有了这些,您可以通过将子解析器与标准接口(@98​​7654357@、ApplicativeAlternativeMonad)相结合,在抽象中表达解析器,而无需讨论拆分输入,例如:

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

这是我在您的情况下推荐的方法。解析器组合器也是在实践中轻松使用应用程序和 monad 的绝佳方式。

【讨论】:

    猜你喜欢
    • 2012-06-13
    • 1970-01-01
    • 2012-02-09
    • 1970-01-01
    • 2011-06-26
    • 2023-03-28
    • 2016-04-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多