【问题标题】:F# idiomatic way of transforming textF# 转换文本的惯用方式
【发布时间】:2014-07-03 06:42:50
【问题描述】:

麦耶罗!因此,我在 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 或 evenchar seq * Queue&lt;char&gt;?或者你甚至会考虑 char seq * String 在适当的转换之后?

【问题讨论】:

标签: f# tail-recursion


【解决方案1】:

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

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]

【讨论】:

    【解决方案2】:

    我一定会看看FSLex。 FSYacc,FParsec。但是,如果您只想标记 seq&lt;char&gt;,您可以使用 sequence expression 以正确的顺序生成标记。重用递归内部函数的想法,并结合序列表达式,我们可以保持尾递归,如下所示,并避免使用可变数据结构等非惯用工具。

    我更改了分隔符以便于调试和函数的签名。这个版本产生一个seq&lt;string&gt;(你的令牌)作为结果,这可能比具有当前令牌和其余文本的元组更容易使用。如果你只想要第一个令牌,你可以拿走人头。请注意,序列是“按需”生成的,即仅在通过序列消耗令牌时才解析输入。如果您需要每个标记旁边的输入文本的其余部分,您可以在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"]
    

    【讨论】:

      猜你喜欢
      • 2019-08-18
      • 1970-01-01
      • 2017-06-24
      • 2017-04-02
      • 1970-01-01
      • 1970-01-01
      • 2019-11-07
      • 1970-01-01
      • 2019-04-20
      相关资源
      最近更新 更多