我正在尝试通过将一些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 可变变量。
答案 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.chunkBySize
,Seq.chunkBySize
和Array.chunkBySize
,如Brad Collins和Scott 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]]
通过玩n
和step
作为溢价,您可以使用它来切割重叠的批次,即滑动窗口,甚至应用于无限序列,如下所示:< / 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#还是很陌生,所以如果我错过任何东西 - 请纠正我,我将不胜感激。