【问题标题】:F#: How do i split up a sequence into a sequence of sequencesF#:我如何将一个序列拆分为一个序列序列
【发布时间】:2010-11-22 05:42:02
【问题描述】:

背景:

我有一系列连续的带时间戳的数据。数据序列中存在数据不连续的间隙。我想创建一种方法将序列拆分为一系列序列,以便每个子序列包含连续数据(在间隙处拆分输入序列)。

约束:

  • 返回值必须是一个序列,以确保元素只根据需要产生(不能使用列表/数组/缓存)
  • 解决方案不能是 O(n^2),可能会排除 Seq.take - Seq.skip 模式(参见Brian's 帖子)
  • 函数式惯用方法的奖励积分(因为我想更加精通函数式编程),但这不是必需的。

方法签名

let groupContiguousDataPoints (timeBetweenContiguousDataPoints : TimeSpan) (dataPointsWithHoles : seq<DateTime * float>) : (seq<seq< DateTime * float >>)= ... 

从表面上看,这个问题对我来说似乎微不足道,但即使使用 Seq.pairwise、IEnumerator<_>、序列推导和 yield 语句,我也无法找到解决方案。我确信这是因为我仍然缺乏结合 F# 习语的经验,或者可能是因为我还没有接触过一些语言结构。

// Test data
let numbers = {1.0..1000.0}
let baseTime = DateTime.Now
let contiguousTimeStamps = seq { for n in numbers ->baseTime.AddMinutes(n)}

let dataWithOccationalHoles = Seq.zip contiguousTimeStamps numbers |> Seq.filter (fun (dateTime, num) -> num % 77.0 <> 0.0) // Has a gap in the data every 77 items

let timeBetweenContiguousValues = (new TimeSpan(0,1,0))

dataWithOccationalHoles |> groupContiguousDataPoints timeBetweenContiguousValues |> Seq.iteri (fun i sequence -> printfn "Group %d has %d data-points: Head: %f" i (Seq.length sequence) (snd(Seq.hd sequence)))

【问题讨论】:

  • 交叉引用:here 是同一个问题,但针对列表。

标签: f# sequences


【解决方案1】:

我认为这是你想要的

dataWithOccationalHoles 
|> Seq.pairwise
|> Seq.map(fun ((time1,elem1),(time2,elem2)) -> if time2-time1 = timeBetweenContiguousValues then 0, ((time1,elem1),(time2,elem2)) else 1, ((time1,elem1),(time2,elem2)) )
|> Seq.scan(fun (indexres,(t1,e1),(t2,e2)) (index,((time1,elem1),(time2,elem2))) ->  (index+indexres,(time1,elem1),(time2,elem2))  ) (0,(baseTime,-1.0),(baseTime,-1.0))
|> Seq.map( fun (index,(time1,elem1),(time2,elem2)) -> index,(time2,elem2) )
|> Seq.filter( fun (_,(_,elem)) -> elem <> -1.0)
|> PSeq.groupBy(fst)
|> Seq.map(snd>>Seq.map(snd))

感谢您提出这个很酷的问题

【讨论】:

    【解决方案2】:

    我将 Alexey 的 Haskell 翻译成 F#,但它在 F# 中并不漂亮,而且还有一个元素过于急切。

    我希望有更好的方法,但我必须稍后再试一次。

    let N = 20
    let data =  // produce some arbitrary data with holes
        seq {
            for x in 1..N do
                if x % 4 <> 0 && x % 7 <> 0 then
                    printfn "producing %d" x
                    yield x
        }
    
    let rec GroupBy comp (input:LazyList<'a>) : LazyList<LazyList<'a>> = 
        LazyList.delayed (fun () ->
        match input with
        | LazyList.Nil -> LazyList.cons (LazyList.empty()) (LazyList.empty())
        | LazyList.Cons(x,LazyList.Nil) -> 
            LazyList.cons (LazyList.cons x (LazyList.empty())) (LazyList.empty())
        | LazyList.Cons(x,(LazyList.Cons(y,_) as xs)) ->
            let groups = GroupBy comp xs
            if comp x y then
                LazyList.consf 
                    (LazyList.consf x (fun () -> 
                        let (LazyList.Cons(firstGroup,_)) = groups
                        firstGroup)) 
                    (fun () -> 
                        let (LazyList.Cons(_,otherGroups)) = groups
                        otherGroups)
            else
                LazyList.cons (LazyList.cons x (LazyList.empty())) groups)
    
    let result = data |> LazyList.of_seq |> GroupBy (fun x y -> y = x + 1)
    printfn "Consuming..."
    for group in result do
        printfn "about to do a group"
        for x in group do
            printfn "  %d" x
    

    【讨论】:

    • Brian,我在尝试对您的代码进行 FSI 时收到以下错误消息,即使我引用了 FSharp.PowerPack.dll。 (我什至可以使用对象浏览器在 PowerPack 中找到 LazyList)“未定义类型‘LazyList’。在 FSharp.PowerPack.dll 中找到了具有此名称的构造,其中包含在某些模块和类型中隐式引用的模块和类型以前版本的 F#。您可能需要添加对此 DLL 的显式引用才能编译此代码。"
    • FSI 看不到项目中的引用;你需要说 #r "FSharp.PowerPack.dll";;在 FSI 窗口中获取该参考。
    【解决方案3】:

    你似乎想要一个有签名的函数

    (`a -> bool) -> seq<'a> -> seq<seq<'a>>
    

    即一个函数和一个序列,然后根据函数的结果将输入序列分解为一个序列序列。

    将值缓存到实现 IEnumerable 的集合中可能是最简单的(尽管不完全是纯粹的,但要避免多次迭代输入。这将失去输入的大部分惰性):

    让 groupBy (fun: 'a -> bool) (输入: seq) = 序列{ 让缓存 = ref (new System.Collections.Generic.List()) for e in input do (!cache).Add(e) 如果不是(fun e)那么 产量!缓存 缓存 := 新 System.Collections.Generic.List() 如果 cache.Length > 0 那么 产量!缓存 }

    另一种实现可以将缓存集合(如seq&lt;'a&gt;)传递给函数,以便它可以看到多个元素来选择断点。

    【讨论】:

    • Richard,我希望能够避免对内部序列使用缓存。
    • 此外,最里面的 let 似乎只在 if 语句中限定范围。你打算让缓存可变吗?
    • @Treefrog:哎呀,是的,它应该是一个参考列表,会更正。
    • @Treefrog:我不认为没有缓存就可以做到这一点:seq 是一个接口,你需要一个具体的类型来产生实例。
    • Alexey,您能否详细说明如何使用嵌套 seq 工作流程?
    【解决方案4】:

    一个 Haskell 解决方案,因为我不太了解 F# 语法,但应该很容易翻译:

    type TimeStamp = Integer -- ticks
    type TimeSpan  = Integer -- difference between TimeStamps
    
    groupContiguousDataPoints :: TimeSpan -> [(TimeStamp, a)] -> [[(TimeStamp, a)]]
    

    Prelude中有一个函数groupBy :: (a -&gt; a -&gt; Bool) -&gt; [a] -&gt; [[a]]

    group 函数接受一个列表并返回一个列表列表,使得结果的连接等于参数。此外,结果中的每个子列表仅包含相等的元素。例如,

    group "Mississippi" = ["M","i","ss","i","ss","i","pp","i"]
    

    它是 groupBy 的一个特例,它允许程序员提供自己的相等性测试。

    这不是我们想要的,因为它将列表中的每个元素与当前组的 first 元素进行比较,并且我们需要比较连续的元素。如果我们有这样的函数groupBy1,我们可以很容易地写成groupContiguousDataPoints

    groupContiguousDataPoints maxTimeDiff list = groupBy1 (\(t1, _) (t2, _) -> t2 - t1 <= maxTimeDiff) list
    

    那就写吧!

    groupBy1 :: (a -> a -> Bool) -> [a] -> [[a]]
    groupBy1 _    []            = [[]]
    groupBy1 _    [x]           = [[x]]
    groupBy1 comp (x : xs@(y : _))
      | comp x y  = (x : firstGroup) : otherGroups
      | otherwise = [x] : groups
      where groups@(firstGroup : otherGroups) = groupBy1 comp xs
    

    更新:看起来 F# 不允许您在 seq 上进行模式匹配,因此它毕竟不太容易翻译。但是,this thread on HubFS 展示了一种模式匹配序列的方法,在需要时将它们转换为 LazyList

    UPDATE2:Haskell 列表惰性的并根据需要生成,因此它们对应于 F# 的 LazyList(而不是 seq,因为生成的数据被缓存(当然,垃圾收集,如果你不再持有它的引用))。

    【讨论】:

    • Alexey,您正在处理输入列表,并生成列表列表的输出。正如我在问题中解释的那样,我需要对序列序列而不是列表列表进行操作,因为在 F# 中,序列是根据需要生成的,而不是立即在内存中构造的列表(这对于非常大的数据集)
    【解决方案5】:

    (编辑:这与 Brian 的解决方案存在类似的问题,因为迭代外部序列而不迭代每个内部序列会严重搞砸!)

    这是一个嵌套序列表达式的解决方案。 .NET 的 IEnumerable&lt;T&gt; 的专横性在这里非常明显,这使得为这个问题编写惯用的 F# 代码有点困难,但希望它仍然清楚发生了什么。

    let groupBy cmp (sq:seq<_>) =
      let en = sq.GetEnumerator()
      let rec partitions (first:option<_>) =
        seq {
          match first with
          | Some first' ->             //'
            (* The following value is always overwritten; 
               it represents the first element of the next subsequence to output, if any *)
            let next = ref None
    
            (* This function generates a subsequence to output, 
               setting next appropriately as it goes *)
            let rec iter item = 
              seq {
                yield item
                if (en.MoveNext()) then
                  let curr = en.Current
                  if (cmp item curr) then
                    yield! iter curr
                  else // consumed one too many - pass it on as the start of the next sequence
                    next := Some curr
                else
                  next := None
              }
            yield iter first' (* ' generate the first sequence *)
            yield! partitions !next (* recursively generate all remaining sequences *)
          | None -> () // return an empty sequence if there are no more values
        }
      let first = if en.MoveNext() then Some en.Current else None
      partitions first
    
    let groupContiguousDataPoints (time:TimeSpan) : (seq<DateTime*_> -> _) = 
      groupBy (fun (t,_) (t',_) -> t' - t <= time)
    

    【讨论】:

    • kvb,我对您成功实现此功能的功能印象深刻(仅使用一个参考单元)。我将研究它以提高我对函数式编程的掌握(递归使我有点难以理解)。感谢您的努力!
    • 哈,我正要评论类似于 Brian 的解决方案的问题 :-) 这正在变成一个真正的脑筋急转弯(不是 Brian-twister)。
    【解决方案6】:

    好的,再试一次。在 F# 中实现最佳的懒惰程度有点困难......从好的方面来说,这比我上次的尝试更实用,因为它不使用任何参考单元。

    let groupBy cmp (sq:seq<_>) =
      let en = sq.GetEnumerator()
      let next() = if en.MoveNext() then Some en.Current else None
      (* this function returns a pair containing the first sequence and a lazy option indicating the first element in the next sequence (if any) *)
      let rec seqStartingWith start =
        match next() with
        | Some y when cmp start y ->
            let rest_next = lazy seqStartingWith y // delay evaluation until forced - stores the rest of this sequence and the start of the next one as a pair
            seq { yield start; yield! fst (Lazy.force rest_next) }, 
              lazy Lazy.force (snd (Lazy.force rest_next))
        | next -> seq { yield start }, lazy next
      let rec iter start =
        seq {
          match (Lazy.force start) with
          | None -> ()
          | Some start -> 
              let (first,next) = seqStartingWith start
              yield first
              yield! iter next
        }
      Seq.cache (iter (lazy next()))
    

    【讨论】:

    • 这不会释放枚举数。乍一看,您可能可以在 next() 的 'else' 分支中执行此操作。
    • 我收到以下异常(使用 VS2010 beta 1):“错误 FS0193:内部错误:编译单元“FSharp.Core”中的模块/命名空间“Microsoft.FSharp.Control”没有包含 val 'Lazy`1.Force.1'" 有什么想法吗?
    • @Treefrog - 我在这台计算机上没有 VS2010,但使用 F# 1.9.6.16 时我没有收到该错误...“内部错误”位使它看起来像一个编译器错误对我来说;也许报告给 fsbugs@microsoft.com 看看他们怎么说?
    【解决方案7】:

    下面是一些我认为你想要的代码。它不是惯用的 F#。

    (可能与 Brian 的回答类似,虽然我不知道,因为我不熟悉 LazyList 语义。)

    但它与您的测试规范不完全匹配:Seq.length 枚举其整个输入。您的“测试代码”调用Seq.length,然后调用Seq.hd。这将生成一个枚举器两次,并且由于没有缓存,所以事情变得一团糟。我不确定是否有任何干净的方法可以允许多个枚举器不进行缓存。坦率地说,seq&lt;seq&lt;'a&gt;&gt; 可能不是解决这个问题的最佳数据结构。

    不管怎样,代码如下:

    type State<'a> = Unstarted | InnerOkay of 'a | NeedNewInner of 'a | Finished
    
    // f() = true means the neighbors should be kept together
    // f() = false means they should be split
    let split_up (f : 'a -> 'a -> bool) (input : seq<'a>) =
        // simple unfold that assumes f captured a mutable variable
        let iter f = Seq.unfold (fun _ -> 
            match f() with
            | Some(x) -> Some(x,())
            | None -> None) ()
    
        seq {
            let state = ref (Unstarted)
            use ie = input.GetEnumerator()
    
            let innerMoveNext() = 
                match !state with
                | Unstarted -> 
                    if ie.MoveNext()
                    then let cur = ie.Current
                         state := InnerOkay(cur); Some(cur)
                    else state := Finished; None 
                | InnerOkay(last) ->
                    if ie.MoveNext()
                    then let cur = ie.Current
                         if f last cur
                         then state := InnerOkay(cur); Some(cur)
                         else state := NeedNewInner(cur); None
                    else state := Finished; None
                | NeedNewInner(last) -> state := InnerOkay(last); Some(last)
                | Finished -> None 
    
            let outerMoveNext() =
                match !state with
                | Unstarted | NeedNewInner(_) -> Some(iter innerMoveNext)
                | InnerOkay(_) -> failwith "Move to next inner seq when current is active: undefined behavior."
                | Finished -> None
    
            yield! iter outerMoveNext }
    
    
    open System
    
    let groupContigs (contigTime : TimeSpan) (holey : seq<DateTime * int>) =
        split_up (fun (t1,_) (t2,_) -> (t2 - t1) <= contigTime) holey
    
    
    // Test data
    let numbers = {1 .. 15}
    let contiguousTimeStamps = 
        let baseTime = DateTime.Now
        seq { for n in numbers -> baseTime.AddMinutes(float n)}
    
    let holeyData = 
        Seq.zip contiguousTimeStamps numbers 
            |> Seq.filter (fun (dateTime, num) -> num % 7 <> 0)
    
    let grouped_data = groupContigs (new TimeSpan(0,1,0)) holeyData
    
    
    printfn "Consuming..."
    for group in grouped_data do
        printfn "about to do a group"
        for x in group do
            printfn "  %A" x
    

    【讨论】:

    • 我认为您对use 关键字的使用导致了两次枚举序列的问题。顺便说一句,我不确定是否有一种简单的方法可以正确处理枚举器,同时仍然允许多次遍历。
    • @kvb,你能详细说明一下吗?我没有尝试运行此代码,但乍一看它对我来说还可以。有没有失败的复制品?
    • 似乎人们在使用这个和其他解决方案时遇到的问题(在第一个完全迭代之前迭代第二个 seq)来自原始问题的错误规格或规格不足:它要求没有缓存。因此,如果消费者在消费完第一个序列之前就开始消费第二个序列,那么生产者(我们都在尝试编写的这段代码)应该为第二个序列产生什么? ...
    • ... 如果第二个 seq 产生当前元素并继续前进,那么第一个 seq 现在是无效的(问问自己,如果消费者继续迭代它(第一个 seq)应该产生什么? )。如果第二个没有产生当前元素,它应该做什么呢?
    • 基本上,seq> 允许消费者做一些没有意义的事情(比如跳过未完成的内部序列),因为基础数据的性质和不被缓存的要求.
    【解决方案8】:

    好的,这是我不满意的答案。

    (编辑:我很不高兴 - 这是错误的!虽然现在没有时间尝试修复。)

    它使用了一些命令式状态,但并不难理解(只要你记得'!' 是 F# 取消引用运算符,而不是 'not')。它尽可能地懒惰,并将一个 seq 作为输入并返回一个 seq 的 seq 作为输出。

    let N = 20
    let data =  // produce some arbitrary data with holes
        seq {
            for x in 1..N do
                if x % 4 <> 0 && x % 7 <> 0 then
                    printfn "producing %d" x
                    yield x
        }
    let rec GroupBy comp (input:seq<_>) = seq {
        let doneWithThisGroup = ref false
        let areMore = ref true
        use e = input.GetEnumerator()
        let Next() = areMore := e.MoveNext(); !areMore
        // deal with length 0 or 1, seed 'prev'
        if not(e.MoveNext()) then () else
        let prev = ref e.Current
        while !areMore do
            yield seq {
                while not(!doneWithThisGroup) do
                    if Next() then
                        let next = e.Current 
                        doneWithThisGroup := not(comp !prev next)
                        yield !prev 
                        prev := next
                    else
                        // end of list, yield final value
                        yield !prev
                        doneWithThisGroup := true } 
            doneWithThisGroup := false }
    let result = data |> GroupBy (fun x y -> y = x + 1)
    printfn "Consuming..."
    for group in result do
        printfn "about to do a group"
        for x in group do
            printfn "  %d" x
    

    【讨论】:

    • Brian,这就是我一直在寻找的 :-) 我自己解决问题的尝试使用了非常相似的方法(嵌套序列理解),但产生了不稳定的结果。起初我认为这是由于序列理解闭包都捕获了相同的参考单元,但我现在发现错误是由于错误的测试数据造成的。我似乎对“DateTime.Now”进行了多次调用,其中只打算调用一次,导致后续的 DateTime 比较失败。顺便说一句 - “if not(e.MoveNext()) then () else ...”似乎等同于更简单的“if e.MoveNext() then...”?
    • 序列表达式用的越多,理解的越少……为什么Seq.length (GroupBy (fun _ _ -&gt; true) [1])会陷入死循环?
    • 另外,似乎没有必要声明 GroupBy "rec",因为它不是递归的 :-)
    • 我在“while !areMore do”中也遇到了一个无限循环。就好像从未输入过“yield seq”语句。
    • 是的;这个解决方案是完全错误的,啊。例如,如果消费者需要外部 seq 的元素,但不消耗内部 seq 的元素,则效果永远不会发生,并且在使用原始列表时也不会取得任何进展。
    猜你喜欢
    • 1970-01-01
    • 2017-10-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-08-09
    • 2018-07-05
    • 1970-01-01
    相关资源
    最近更新 更多