F#转换文本的惯用方法

时间:2014-05-15 01:18:38

标签: f# tail-recursion

Myello!所以我在F#中寻找一种简洁,高效的惯用方法来解析文件或字符串。我强烈倾向于将输入视为char(char seq)序列。我们的想法是每个函数负责解析一段输入,返回转换后的文本与未使用的输入一起使用,并由更高级别的函数调用,该函数将未使用的输入链接到以下函数并使用结果构建一个化合物类型。因此,每个解析函数都应具有与此类似的签名:char seq - > char seq *'a。例如,如果函数的职责只是提取第一个单词,那么,一种方法如下:

let parseFirstWord (text: char seq) =
  let rec forTailRecursion t acc =
    let c = Seq.head t
    if c = '\n' then
      (t, acc)
    else
      forTailRecursion (Seq.skip 1 t) (c::acc)
  let rest, reversedWord = forTailRecursion text []
  (rest, List.reverse reversedWord)

现在,当然,这种方法的主要问题是它以相反的顺序提取单词,因此你必须反转它。然而,它的主要优点是使用严格的功能特性和适当的尾递归。可以避免在丢失尾递归的同时反转提取的值:

let rec parseFirstWord (text: char seq) =
  let c = Seq.head t
  if c = '\n' then
    (t, [])
  else
    let rest, tail = parseFirstWord (Seq.skip 1 t)
    (rest, (c::tail))

或者在下面使用快速可变数据结构,而不是使用纯粹的功能,例如:

let parseFirstWord (text: char seq) =
  let rec forTailRecursion t queue =
    let c = Seq.head t
    if c = '\n' then
      (t, queue)
    else
      forTailRecursion (Seq.skip 1 t) (queue.Enqueu(c))
  forTailRecursion text (new Queue<char>())

我不知道如何在F#中使用OO概念,因此欢迎您对上述代码进行更正。

对这种语言不熟悉,我希望能够引导F#开发人员通常做出的妥协。建议的方法和你自己的方法,我应该考虑更惯用,为什么?此外,在特定情况下,您将如何封装返回值:char seq * char seq,char seq * char list或甚至char seq * Queue<char>?或者您是否会在正确转换后考虑char seq * String?

2 个答案:

答案 0 :(得分:3)

我肯定会看一下FSLexFSYaccFParsec。但是,如果您只想标记seq<char>,则可以使用sequence expression以正确的顺序生成令牌。重用你的递归内部函数的想法,并结合序列表达式,我们可以保持尾递归,如下所示,并避免像可变数据结构这样的非惯用工具。

我更改了分隔符char以便于调试和功能的签名。此版本生成seq<string>(您的标记),这可能比使用当前标记和文本其余部分的元组更容易使用。如果你只想要第一个令牌,你可以拿走头。注意,序列是“按需”生成的,即仅在令牌通过序列消耗时才解析输入。如果您需要每个令牌旁边的输入文本的其余部分,您可以在loop中产生一对,但我猜测下游消费者很可能不会(此外,如果输入文本本身是懒惰的序列,可能链接到一个流,我们不想暴露它,因为它应该只在一个地方迭代)。

let parse (text : char seq) = 
    let rec loop t acc = 
        seq {
            if Seq.isEmpty t then yield acc
            else
                let c, rest = Seq.head t, Seq.skip 1 t
                if c = ' ' then 
                    yield acc
                    yield! loop rest ""
                else yield! loop rest (acc + string c)
        }
    loop text ""

parse "The FOX is mine"
val it : seq<string> = seq ["The"; "FOX"; "is"; "mine"]

这不是F#中唯一的“惯用”方式。每次我们需要处理序列时,我们都可以查看Seq模块中提供的函数。其中最常见的是fold,它迭代序列一次,通过运行给定函数在每个元素上累积状态。在下面的示例中,accumulate是一个这样的函数,它逐步构建生成的标记序列。由于Seq.fold没有在空序列上运行累加器函数,我们需要最后两行从函数的内部累加器中提取最后一个标记。
第二个实现保留了第一个很好的特性,即尾递归(在fold实现中,如果我没有记错的话)和按需处理输入序列。它也恰好更短,虽然可能性稍差。

let parse2 (text : char seq) =
    let accumulate (res, acc) c =
        if c = ' ' then (Seq.append res (Seq.singleton acc), "")
        else (res, acc + string c)
    let (acc, last) = text |> Seq.fold accumulate (Seq.empty, "")
    Seq.append acc (Seq.singleton last)

parse2 "The FOX is mine"
val it : seq<string> = seq ["The"; "FOX"; "is"; "mine"]

答案 1 :(得分:2)

以F#真正独特的方式进行lexing /解析的一种方法是使用活动模式。以下简化示例显示了一般概念。它可以处理任意长度的计算字符串而不会产生堆栈溢出。

let rec (|CharOf|_|) set = function
    | c :: rest when Set.contains c set -> Some(c, rest)
    | ' ' :: CharOf set (c, rest) -> Some(c, rest)
    | _ -> None

let rec (|CharsOf|) set = function
    | CharOf set (c, CharsOf set (cs, rest)) -> c::cs, rest
    | rest -> [], rest

let (|StringOf|_|) set = function
    | CharsOf set (_::_ as cs, rest) -> Some(System.String(Array.ofList cs), rest)
    | _ -> None

type Token =
    | Int of int
    | Add | Sub | Mul | Div | Mod
    | Unknown

let lex: string -> _ =
    let digits = set ['0'..'9']
    let ops = Set.ofSeq  "+-*/%"

    let rec lex chars =
        seq { match chars with
              | StringOf digits (s, rest) -> yield Int(int s); yield! lex rest
              | CharOf ops (c, rest) -> 
                  let op = 
                      match c with
                      | '+' -> Add | '-' -> Sub | '*' -> Mul | '/' -> Div | '%' -> Mod
                      | _ -> failwith "invalid operator char"
                  yield op; yield! lex rest
              | [] -> ()
              | _ -> yield Unknown }

    List.ofSeq >> lex

lex "1234 + 514 / 500"
// seq [Int 1234; Add; Int 514; Div; Int 500]