最常用的方法是在F#中编写批量大小的seq

时间:2011-09-22 05:19:21

标签: linq f# functional-programming sequence

我正在尝试通过将一些C#算法重写为惯用的F#来学习F#。

我正在尝试重写的第一个函数之一是batchesOf,其中:

[1..17] |> batchesOf 5

将序列分成多个批次,每个最多五个,即:

[[1; 2; 3; 4; 5]; [6; 7; 8; 9; 10]; [11; 12; 13; 14; 15]; [16; 17]]

我第一次尝试这样做有点难看,因为在尝试在闭包内使用 mutable 类型时遇到错误,我使用了一个可变的 ref 对象。使用 ref 特别不愉快,因为要取消引用它你必须使用运算符,当在条件表达式内部时,对于某些将其读取为逻辑不。我遇到的另一个问题是Seq.skip和Seq.take与他们的Linq别名不同,如果 size 超过序列的大小,他们将抛出错误。

let batchesOf size (sequence: _ seq) : _ list seq =
    seq {
        let s = ref sequence
        while not (!s |> Seq.isEmpty)  do
            yield !s |> Seq.truncate size |> List.ofSeq
            s := System.Linq.Enumerable.Skip(!s, size)
    }

无论如何,在F#中重写这个最优雅/惯用的方法是什么?保持原始行为,但最好没有 ref 可变变量。

10 个答案:

答案 0 :(得分:15)

使用seq<_>类型惯用实现此函数很困难 - 该类型本质上是可变的,因此没有简单的好功能方式。您的版本效率很低,因为它会对序列重复使用Skip。更好的命令式选项是使用GetEnumerator并使用IEnumerator迭代元素。您可以在此代码段中找到各种必要选项:http://fssnip.net/1o

如果您正在学习F#,那么最好尝试使用F#list type编写函数。这样,您就可以使用惯用的功能风格。然后你可以使用模式匹配来编写batchesOf,如下所示:递归和累加器参数:

let batchesOf size input = 
  // Inner function that does the actual work.
  // 'input' is the remaining part of the list, 'num' is the number of elements
  // in a current batch, which is stored in 'batch'. Finally, 'acc' is a list of
  // batches (in a reverse order)
  let rec loop input num batch acc =
    match input with
    | [] -> 
        // We've reached the end - add current batch to the list of all
        // batches if it is not empty and return batch (in the right order)
        if batch <> [] then (List.rev batch)::acc else acc
        |> List.rev
    | x::xs when num = size - 1 ->
        // We've reached the end of the batch - add the last element
        // and add batch to the list of batches.
        loop xs 0 [] ((List.rev (x::batch))::acc)
    | x::xs ->
        // Take one element from the input and add it to the current batch
        loop xs (num + 1) (x::batch) acc
  loop input 0 [] []

作为一个脚注,使用计算表达式来处理IEnumerator时,命令式版本可以更好一些,但这不是标准的,而且是非常高级的技巧(例如,请参阅http://fssnip.net/37)。

答案 1 :(得分:10)

一位朋友不久就问我了。这是一个回收的答案。这是有效的,纯粹的:

let batchesOf n =
    Seq.mapi (fun i v -> i / n, v) >>
    Seq.groupBy fst >>
    Seq.map snd >>
    Seq.map (Seq.map snd)

或不纯的版本:

let batchesOf n =
    let i = ref -1
    Seq.groupBy (fun _ -> i := !i + 1; !i / n) >> Seq.map snd

这些产生seq<seq<'a>>。如果您的样本中确实必须有'a list list,那么只需添加... |> Seq.map (List.ofSeq) |> List.ofSeq,如下所示:

> [1..17] |> batchesOf 5 |> Seq.map (List.ofSeq) |> List.ofSeq;;
val it : int list list = [[1; 2; 3; 4; 5]; [6; 7; 8; 9; 10]; [11; 12; 13; 14; 15]; [16; 17]]

希望有所帮助!

答案 2 :(得分:4)

如果你想要

,这可以在没有递归的情况下完成
[0..20] 
    |> Seq.mapi (fun i elem -> (i/size),elem) 
    |> Seq.groupBy (fun (a,_) -> a)
    |> Seq.map (fun (_,se) -> se |> Seq.map (snd));;
val it : seq<seq<int>> =
  seq
    [seq [0; 1; 2; 3; ...]; seq [5; 6; 7; 8; ...]; seq [10; 11; 12; 13; ...];
     seq [15; 16; 17; 18; ...]; ...]

取决于您的想法,这可能更容易理解。托马斯的解决方案可能更加惯用F#,但

答案 3 :(得分:4)

Hurray,我们可以在F {4中使用List.chunkBySizeSeq.chunkBySizeArray.chunkBySize,如Brad CollinsScott Wlaschin所述。

答案 4 :(得分:1)

这可能不是惯用语,但它有效:

let batchesOf n l = 
    let _, _, temp', res' = List.fold (fun (i, n, temp, res) hd ->
                                           if i < n then
                                             (i + 1, n, hd :: temp, res)
                                           else
                                             (1, i, [hd], (List.rev temp) :: res)) 
                                       (0, n, [], []) l
    (List.rev temp') :: res' |> List.rev

答案 5 :(得分:1)

以下是序列的简单实现:

let chunks size (items:seq<_>) =
  use e = items.GetEnumerator()
  let rec loop i acc =
    seq {
      if i = size then 
        yield (List.rev acc)
        yield! loop 0 []
      elif e.MoveNext() then
        yield! loop (i+1) (e.Current::acc)
      else
        yield (List.rev acc)
    }
  if size = 0 then invalidArg "size" "must be greater than zero"
  if Seq.isEmpty items then Seq.empty else loop 0 []

let s = Seq.init 10 id
chunks 3 s 
//output: seq [[0; 1; 2]; [3; 4; 5]; [6; 7; 8]; [9]]

答案 6 :(得分:1)

我的方法涉及将列表转换为数组并递归地分块数组:

    let batchesOf (sz:int) lt = 
        let arr = List.toArray lt

        let rec bite curr =
            if (curr + sz - 1 ) >= arr.Length then
                [Array.toList arr.[ curr .. (arr.Length - 1)]]
            else
                let curr1 = curr + sz 
                (Array.toList (arr.[curr .. (curr + sz - 1)])) :: (bite curr1)   

        bite 0

    batchesOf 5 [1 .. 17]

    [[1; 2; 3; 4; 5]; [6; 7; 8; 9; 10]; [11; 12; 13; 14; 15]; [16; 17]]

答案 7 :(得分:1)

我发现这是一个非常简洁的解决方案:

let partition n (stream:seq<_>) = seq {
    let enum = stream.GetEnumerator()

    let rec collect n partition =
        if n = 1 || not (enum.MoveNext()) then
            partition
        else
            collect (n-1) (partition @ [enum.Current])

    while enum.MoveNext() do
        yield collect n [enum.Current]
}

它适用于序列并产生序列。输出序列由输入序列中的n个元素列表组成。

答案 8 :(得分:0)

您可以使用以下Clojure partition library function的模拟来解决您的任务:

let partition n step coll =
    let rec split ss =
        seq {
            yield(ss |> Seq.truncate n)
            if Seq.length(ss |> Seq.truncate (step+1)) > step then
                yield! split <| (ss |> Seq.skip step)
            }
    split coll

用作partition 5 5它会为您提供所需的batchesOf 5功能:

[1..17] |> partition 5 5;;
val it : seq<seq<int>> =
  seq
    [seq [1; 2; 3; 4; ...]; seq [6; 7; 8; 9; ...]; seq [11; 12; 13; 14; ...];
     seq [16; 17]]

通过玩nstep作为溢价,您可以使用它来切割重叠的批次,即滑动窗口,甚至应用于无限序列,如下所示:< / p>

Seq.initInfinite(fun x -> x) |> partition 4 1;;
val it : seq<seq<int>> =
  seq
    [seq [0; 1; 2; 3]; seq [1; 2; 3; 4]; seq [2; 3; 4; 5]; seq [3; 4; 5; 6];
     ...]

将其视为仅原型,因为它对源序列进行了许多冗余评估,并且不太适合生产目的。

答案 9 :(得分:0)

这个版本通过了我能想到的所有测试,包括用于延迟评估和单序列评估的测试:

let batchIn batchLength sequence =
    let padding = seq { for i in 1 .. batchLength -> None } 
    let wrapped = sequence |> Seq.map Some
    Seq.concat [wrapped; padding]
    |> Seq.windowed batchLength 
    |> Seq.mapi (fun i el -> (i, el)) 
    |> Seq.filter (fun t -> fst t % batchLength = 0) 
    |> Seq.map snd
    |> Seq.map (Seq.choose id)
    |> Seq.filter (fun el -> not (Seq.isEmpty el))

我对F#还是很陌生,所以如果我错过任何东西 - 请纠正我,我将不胜感激。