【问题标题】:FSharp sequence processing with state带状态的 FSharp 序列处理
【发布时间】:2018-06-17 12:20:01
【问题描述】:

我需要从带有文本行的长序列中过滤数据。 文本行形成如下记录:

{  
    BEGINTYPE1    
    VAL1: xxx
    VAL2: yyy
    ENDTYPE1

    // mix of record types including TYPE1
}

我需要在处理过程中保持状态:

  1. 找到记录类型,从而跳过其他文本
  2. 过滤相关值,直到找到记录结尾
  3. 继续 1

我只能用一个列表来做到这一点,因为一个序列似乎 用一种表达方式读到最后。 它“似乎”您不能处理序列的一部分并在另一个表达式中继续使用序列“指针”在它停止的位置? 所以我使用了一个列表。 我的问题,这个处理可以用一个序列来完成吗 使用标准功能,如 Skip、filter ... 等?

我的列表解决方案:

let patLst =  [    
    "VAL1:"         ; 
    "VAL2:"         ; 
    // ..
    ]

let BeginRecord1 = "BEGINTYPE1"
let EndRecord1   = "ENDTYPE1"

let filter (lines:seq<string>) = 
  let llines = Seq.toList lines

  let matchLine inp =  
     let rec loop pat = 
        match pat with 
        | [] -> None
        | h::t -> 
            let m = Regex.Match(inp, h)
            match m.Success with
            | true -> Some (h)
            | _ -> loop t

     loop patLst

  let rec findItem i l = 
    match l with 
    | []    -> []
    | h::t  -> if h=i then  t
               else findItem i t 

  let findItemsUntil u a l =
    let rec loop a l = 
        match l with 
        | []    ->  ([],a)
        | h::t when h=u -> (t , ""::a)
        | h::t -> match matchLine h with
                    | Some(m)  -> loop (m::a) t
                    | None -> loop a t
    loop a l

  let rec loop a l = 
    match findItem  BeginRecord1 l with
    | [] -> List. rev a
    | l2 -> let (l3,a) = findItemsUntil EndRecord1 a l2 
            loop a l3

  llines |> loop  [""] |> List.fold (fun a x -> a + "\n" + x) ""  

}

【问题讨论】:

  • 你能格式化你的代码吗?

标签: list f# sequence state


【解决方案1】:

瞄准
根据示例代码,这可能不是您正在寻找的内容,但我认为通过序列进行单次迭代并将记录映射到具体类型会很有趣。

说明
此解决方案使用一个状态机,它可以位于StartCollecting。在Start 中,它需要一个“BEGINTYPEx”。当发现它将进入Collecting 状态,该状态将属性收集到Map。当收集状态达到“ENDTYPEx”时,它使用映射函数创建一个实例并将其添加到Aggregate list,返回到Start 状态。

实施
为记录定义一些类型,包括这些记录的可区分联合和折叠的状态类型:

type Type1 = {
    val1:string
    val2:string
}

type Type2 = {
    val1:string
    val2:string
}

type Aggregate =
| T1 of Type1
| T2 of Type2

type State =
| Start of Aggregate list
| Collecting of Aggregate list * string * (Map<string,string> -> Aggregate) * Map<string,string>

定义一些映射函数以将Map 映射到记录类型:

let mapType1 (dic:Map<string,string>) = 
    Aggregate.T1 
        {
            val1 = dic.["VAL1"]
            val2 = dic.["VAL2"]
        }

let mapType2 (dic:Map<string,string>) = 
    Aggregate.T2
        {
            val1 = dic.["VAL1"]
            val2 = dic.["VAL2"]
        }

接下来我们有一些活动模式可以轻松决定匹配:

let (|Begin|_|) input =        
    match input with
        | "BEGINTYPE1" -> Some ("TYPE1", mapType1)
        | "BEGINTYPE2" -> Some ("TYPE2", mapType2)
        | _ -> None

let (|Prop|_|) input =        
    if(String.IsNullOrEmpty(input)) then None
    else 
        if(input.Contains(":")) then
            let split = input.Split(":")
            let pName = split.[0].Trim()
            let pValue = split.[1].Trim()
            Some (pName,pValue)
        else None

let (|End|_|) (l,label,f,m) input =        
    match input with
        | "ENDTYPE1" -> Some (List.append l ([f m]), label)
        | "ENDTYPE2" -> Some (List.append l ([f m]), label)
        | _ -> None

从一种状态移动到下一种状态的实际文件夹功能:

let folder state line =
    match state with
    | Start xs -> 
        match line with
        | Begin (label, f) -> Collecting (xs, label, f, Map.empty<string,string>)
        | _ -> failwithf "Should start with a BEGINTYPEx, intead was %s" line
    | Collecting (xs, label, f, m) -> 
        match line with
        | Prop (k,v) -> Collecting (xs, label, f, Map.add k v m)
        | End(xs, label, f, m) (ys, s) -> Start ys
        | _ -> failwithf "Expecting property or ENDTYPEx, instead was %s" line

帮助轻松提取列表的简单辅助方法:

let extractTypeList state =
    match state with
    | Start xs -> xs
    | Collecting (xs, _,_,_) -> xs

最后,用法:

let lines = seq {
        yield "BEGINTYPE1"
        yield "VAL1: xxx"
        yield "VAL2: yyy"
        yield "ENDTYPE1"
        yield "BEGINTYPE2"
        yield "VAL1: xxx"
        yield "VAL2: yyy"
        yield "ENDTYPE2"
    }

let extractTypes lines = 
    lines 
    |> Seq.fold folder (Start [])
    |> extractTypeList
    |> List.iter (fun a -> printfn "%A" a)

extractTypes lines |> ignore

一些有用的链接:

了解Active Patterns
了解fold

【讨论】:

  • 感谢您的回复。我首先必须研究您的解决方案才能真正理解。
  • @RobF 如果您有任何问题,请大声疾呼。如果您想将其作为 fsx 完整获取(以及对该要点的评论中的控制台版本),我会将代码放在一个要点上gist.github.com/dburriss/5407a37f08127679750a73e639665ab5
  • @RobF 在实际给出实现之前,我编辑了答案以提供更多对解决方案方法的描述。希望它对您有所帮助并祝您编码愉快。
  • 你好,我理解折叠函数中处理列表项并将状态保持在折叠累加器中的想法,从未想过这个,聪明的解决方案!
【解决方案2】:

您可以使用与列表几乎相同的方式来处理序列,您只需要使用Seq.headSeq.tail 函数,而不是使用可用于列表的便捷模式匹配语法。使用内置函数,您的解决方案将如下所示:

open System.Text.RegularExpressions

let patLst =  [    
    "VAL1:"         ; 
    "VAL2:"         ; 
    // ..
    ]

let BeginRecord1 = "BEGINTYPE1"
let EndRecord1   = "ENDTYPE1"

let filter (lines:seq<string>) = 
  let matchLine inp =  
     let rec loop pat = 
        match pat with 
        | [] -> None
        | h::t -> 
            match Regex.Match(inp, h) with
            | m when m.Success -> Some (h)
            | _ -> loop t

     loop patLst

  let rec findItem i l = 
    if l |> Seq.isEmpty
    then Seq.empty
    else let h = l |> Seq.head  
         let t = l |> Seq.tail
         if h=i 
         then t
         else findItem i t 

  let findItemsUntil u a l =
    let rec loop a l = 
        if l |> Seq.isEmpty
        then (Seq.empty,a)
        else let h = l |> Seq.head
             let t = l |> Seq.tail
             if h=u 
             then (t , ""::a)
             else match matchLine h with
                  | Some(m)  -> loop (m::a) t
                  | None -> loop a t
    loop a l

  let rec loop a l = 
    match findItem  BeginRecord1 l with
    | s when s |> Seq.isEmpty -> List.rev a
    | l2 -> let (l3,a) = findItemsUntil EndRecord1 a l2 
            loop a l3

  lines |> loop  [""] |> List.fold (fun a x -> a + "\n" + x) ""  

现在,如果您想简化逻辑,您可以编写自己的 Active Pattern 来执行与列表的 head :: tail 模式相同的操作。活动模式本身看起来像这样:

let (|HT|Empty|) s =
    match s |> Seq.tryHead with
    | Some head -> HT (head, s |> Seq.tail)
    | None -> Empty

然后您的实现可以与您的基于列表的版本几乎保持一致,只需交换此活动模式并将空列表替换为Seq.empty

let filter (lines:seq<string>) = 
  let matchLine inp =  
     let rec loop pat = 
        match pat with 
        | Empty -> None
        | HT (h,t) -> 
            let m = Regex.Match(inp, h)
            match m.Success with
            | true -> Some (h)
            | _ -> loop t

     loop patLst

  let rec findItem i l = 
    match l with 
    | Empty -> Seq.empty
    | HT (h,t) -> if h=i then  t
                  else findItem i t 

  let findItemsUntil u a l =
    let rec loop a l = 
        match l with 
        | Empty -> (Seq.empty,a)
        | HT (h,t) when h=u -> (t , ""::a)
        | HT (h,t) -> match matchLine h with
                      | Some(m) -> loop (m::a) t
                      | None -> loop a t
    loop a l

  let rec loop a l = 
    match findItem  BeginRecord1 l with
    | Empty -> List.rev a
    | l2 -> let (l3,a) = findItemsUntil EndRecord1 a l2 
            loop a l3

  lines |> loop  [""] |> List.fold (fun a x -> a + "\n" + x) ""  

【讨论】:

  • 谢谢!我不知道可以这样使用序列。我现在将首先对 List 和 Sequence 版本进行性能测试。
  • 令我惊讶的是:对于某些文件大小:使用序列处理:18.000 毫秒使用列表处理:43 毫秒代码相同:在两种情况下:让 flines = File.ReadAllLines 文件列表案例:让行 = Seq.toList 行
  • @RobF 序列被延迟评估,根据使用情况,这可能是一种好处或一种惩罚。在您的情况下,听起来序列被遍历了很多次。如果序列是通过从文件中读取行来构建的,这将非常昂贵。在这种情况下,几乎可以肯定的是,从文件中读取行一次并将它们存储在一个列表中,以便对其进行热切评估。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-08-08
  • 1970-01-01
  • 2017-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多