【问题标题】:Most idiomatic way to write batchesOf size seq in F#在 F# 中编写 batchesOf size seq 的最惯用方式
【发布时间】:2011-09-22 05:19:21
【问题描述】:

我正在尝试通过将一些 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 可变变量。

【问题讨论】:

标签: linq f# functional-programming sequence


【解决方案1】:

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

如果您正在学习 F#,那么最好尝试使用 F# 列表类型编写函数。这样,您可以使用惯用的功能样式。然后您可以使用带有递归和累加器参数的模式匹配来编写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,但这不是标准的,它是相当高级的技巧(例如,参见 @987654322 @)。

【讨论】:

  • 非常感谢您的详细解释以及替代方案的链接!
【解决方案2】:

一位朋友不久前问过我这个问题。这是一个回收的答案。这很有效并且很纯粹:

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&lt;seq&lt;'a&gt;&gt;。如果您真的必须在您的示例中使用'a list list,那么只需添加... |&gt; Seq.map (List.ofSeq) |&gt; 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]]

希望有帮助!

【讨论】:

  • +1 用于在顶部示例中编写最优雅的代码。我会默认使用它,除非有明确的迹象表明应用它的代码位置与代码中的其他位置相比性能相对关键,并且性能差异实际上是用户可以感知的。
  • 是的,这很纯粹,但它并不懒惰。 groupBy 调用急切地消耗整个输入序列,这与首先批处理项目的目的相冲突。
【解决方案3】:

如果你愿意,这可以在没有递归的情况下完成

[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; ...]; ...]

取决于您认为这可能更容易理解。不过,Tomas 的解决方案可能更符合 F# 的习惯

【讨论】:

  • 使用groupBy 是一个非常巧妙的技巧 - 我认为这是初始原型实现的好方法(您可以快速轻松地完成)。我想性能会有点问题。递归版本可能会更好一些,我提到的 sn-p 中的命令式解决方案会更快。但在进行原型设计和测试时,性能通常不是问题。
  • 请注意,groupBy 可能无法保证它会保留元素的顺序。当我发现 PSeq 函数不确定地重新排序您的序列时,我感到非常惊讶......
【解决方案4】:

万岁,我们可以在 F# 4 中使用List.chunkBySizeSeq.chunkBySizeArray.chunkBySize,正如Brad CollinsScott Wlaschin 所提到的那样。

【讨论】:

    【解决方案5】:

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

    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
    

    【讨论】:

    • temp' 和 res' 上的单引号是 F# 约定吗?
    • 不,在这种情况下我不这么认为。我只是没有想出更多描述性名称的想象力:-)
    【解决方案6】:

    这是一个简单的序列实现:

    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]]
    

    【讨论】:

      【解决方案7】:

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

          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]]
      

      【讨论】:

        【解决方案8】:

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

        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 个元素的列表组成。

        【讨论】:

          【解决方案9】:

          您可以使用下面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 作为一项额外功能,您可以使用它来切片重叠批次,也就是滑动窗口,甚至可以应用于无限序列,如下所示:

          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];
               ...]
          

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

          【讨论】:

            【解决方案10】:

            这个版本通过了我能想到的所有测试,包括惰性求值和单序列求值:

            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# 还是很陌生,所以如果我遗漏了什么 - 请纠正我,我们将不胜感激。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-05-01
              • 1970-01-01
              • 2014-07-03
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多