在F#中将令牌序列解析为分层类型

时间:2014-10-28 15:03:55

标签: parsing f#

我处理了一些HTML以从网站中提取各种信息(那里没有适当的API),并使用F#区分联合生成一个令牌列表。我已将代码简化为本质:

type tokens =
  | A of string
  | B of int
  | C of string

let input = [A "1"; B 2; C "2.1"; C "2.2"; B 3; C "3.1"]

// how to transform the input to the following ???

let desiredOutput = [A "1", [[ B 2, [ C "2.1"; C "2.2" ]]; [B 3, [ C "3.1" ]]]]

这大致对应于解析语法:g - > A b *; b - > B c *; C-> C

关键是我的令牌列表是扁平的,但我想使用语法隐含的层次结构。

也许还有我想要的输出的另一种表现形式会更好;我真正想做的是处理一个A,然后是零个或多个B序列,恰好包含零个或多个Cs。

我查看过解析器组合器文章,例如关于FParsec,但我找不到一个好的解决方案,允许我从一个令牌列表而不是一个字符串开始。我熟悉解析的命令性技术,但我不知道什么是惯用的F#。

由于答案而取得的进展

感谢Vandroiy的回答,我能够编写以下内容来推进我正在努力学习惯用语F#(以及刮掉测验网站)的爱好项目。

// transform flat data scraped from a Quiz website into a hierarchical data structure

type ScrapedQuiz =
  | Title of string
  | Description of string
  | Blurb of string * picture: string
  | QuizId of string
  | Question of num:string * text:string * picture : string
  | Answer of text:string
  | Error of exn

let input = 
  [Title "Example Quiz Scraped from a website";
   Description "What the Quiz is about";
   Blurb ("more details","and a URL for a picture");
   Question ("#1", "How good is F#", "URL to picture of new F# logo");
   Answer ("we likes it");
   Answer ("we very likes it");
   Question ("#2", "How useful is Stack Overflow", "URL to picture of Stack Overflow logo");
   Answer ("very good today");
   Answer ("lobsters");
  ]

type Quiz =
  { Title : string
    Description : string
    Blurb : string * PictureURL 
    Questions : Quest list }
and Quest =
  { Number : string
    Text : string
    Pic : PictureURL
    Answers : string list}
and PictureURL = string

let errorMessage = "unexpected input format"

let parseList reader input =
  let rec run acc inp =
    match reader inp with
    | Some(o, inp') -> run (o :: acc) inp'
    | None -> List.rev acc, inp
  run [] input

let readAnswer = function Answer(a) :: t -> Some(a, t) | _ -> None

let readDescription = 
                function Description(a) :: t -> (a, t) | _ -> failwith errorMessage
let readBlurb = function Blurb(a,b) :: t -> ((a,b),t)  | _ -> failwith errorMessage

let readQuests = function
  | Question(n,txt,pic) :: t ->
      let answers, input' = parseList readAnswer t
      Some( { Number=n; Text=txt; Pic=pic; Answers = answers}, input')
  | _ -> None

let readQuiz = function
  | Title(s) :: t ->
      let d,  input'   = readDescription t
      let b,  input''  = readBlurb input'
      let qs, input''' = parseList readQuests input''
      Some( { Title = s; Description = d; Blurb = b; Questions = qs}, input''')
  | _ -> None

match readQuiz input with
  | Some(a, []) -> a
  | _ -> failwith errorMessage

我昨天写不出来;既不是目标数据类型,也不是解析代码。我看到了改进的余地,但我认为我已经开始实现不在F#中编写C#的目标。

2 个答案:

答案 0 :(得分:2)

事实上,首先找到一个好的代表可能会有所帮助。

原始输出格式

我认为在标准打印中建议的输出形式是:

[(A "1", [(B 2, [C "2.1"; C "2.2"]); (B 3, [C "3.1"])])]

(这与列表级别数量中的问题不同。)我用来实现的代码很难看。在某种程度上,这是因为它在一个尴尬的位置进行抽象,将输入和输出类型限制在很远的位置而不给它们一个明确定义的类型。我是为了完整而发布的,但我建议跳过它。

let rec readBranch checkOne readInner acc = function
    | h :: t when checkOne h ->
        let dat, inp' = readInner t
        readBranch checkOne readInner ((h, dat) :: acc) inp'
    | l -> List.rev acc, l

let rec readCs acc = function
    | C(s) :: t -> readCs (C(s) :: acc) t
    | l -> List.rev acc, l

let readBs = readBranch (function B _ -> true | _ -> false) (readCs []) []
let readAs = readBranch (function A _ -> true | _ -> false) readBs []

input |> readAs |> fst

当然,其他人可以更明智地做到这一点,但我怀疑它会解决主要问题:我们只是将一个奇怪的数据结构投射到下一个。如果难以阅读或制定解析器的输出格式,可能会出现问题。

强类型输出

我宁愿首先关注我们正在解析的内容,而不是专注于我们正在解析的 。这些A B C对我来说没有任何意义。假设它们代表了对象:

type Bravo =
    { ID : int
      Charlies : string list }

type Alpha =
    { Name : string
      Bravos : Bravo list }

有两个地方可以解析相同类型的对象序列。让我们创建一个帮助器,重复使用特定的解析器来读取对象列表:

/// Parses objects into a list. reader takes an input and returns either
/// Some(parsed item, new input state), or None if the list is finished.
/// Returns a list of parsed objects and the remaining input.
let parseList reader input =
    let rec run acc inp =
        match reader inp with
        | Some(o, inp') -> run (o :: acc) inp'
        | None -> List.rev acc, inp
    run [] input

请注意,这在input类型中非常通用。这个帮助器可以用于字符串,序列或其他任何东西。

现在,我们添加具体的解析器。以下函数在助手的reader中使用了签名;它们要么返回已解析的对象,要么返回剩余的输入,如果无法解析则返回None。

let readC = function C(s) :: t -> Some(s, t) | _ -> None

let readB = function
    | B(i) :: t ->
        let charlies, input' = parseList readC t
        Some( { ID = i; Charlies = charlies }, input' )
    | _ -> None

let readA = function
    | A(s) :: t ->
        let bravos, input' = parseList readB t
        Some( { Name = s; Bravos = bravos }, input' )
    | _ -> None

阅读Alphas和Bravos的代码实际上是重复的。如果在生产代码中发生这种情况,我建议再次检查数据结构是否最佳,然后再看看改进算法。

我们要求将一个A读入一个Alpha,毕竟这是目标:

match readA input with
| Some(a, []) -> a
| _ -> failwith "Unexpected input format"

可能有许多更好的方法来进行解析,尤其是在了解更多有关确切问题的情况时。重要的事实不是解析器的工作原理,而是输出的样子,这将是程序中实际工作完成时的重点。第二个版本的输出应该更容易在代码和调试器中导航:

val it : Alpha =
    { Name = "1";
      Bravos = [ { ID = 2; Charlies = ["2.1"; "2.2"] }
                 { ID = 3; Charlies = ["3.1"] } ] }

可以更进一步,用DOM(文档对象模型)替换标记化的数据结构。然后,第一步是使用标准解析库将HTML读入DOM。在第二步中,具体解析器将构造对象,使用DOM表示作为输入,从上到下相互调用。

答案 1 :(得分:0)

要使用结构化层次结构,您必须创建匹配的类型结构。像

这样的东西
type 
   RootType = Level1 list
and
   Level1 = 
     | A of string
     | B of Level2 list
     | C of string
and 
   Level2 = 
     { b: int; c: string list }