在F#中,是否有一种将平面项目数组转换为一组项目数组的功能方法?

时间:2011-08-12 17:38:49

标签: f#

在F#中,假设我们有一个字节数组,表示按RGB顺序每像素三个字节的像素数据:

[| 255; 0;   0; //Solid red
   0;   255; 0; //Solid green
   0;   0;   255; //Solid blue
   1;   72;  9; 
   34;  15;  155
... |]

我很难知道如何按原样对这些数据进行功能操作,因为单个项目实际上是数组中三个元素的连续块。

所以,我需要先将数组中的三元组分组为:

[| 
   [| 255; 0;   0   |];
   [| 0;   255; 0   |];
   [| 0;   0;   255 |];
   [| 1;   72;  9   |];
   [| 34;  15;  155 |]
... |]

现在,将三元组收集到子数组中很容易用for循环,但我很好奇 - 是否有一种功能方法来收集F#中的数组元素组?我的最终目标不仅仅是如上所述转换数据,而是以更具声明性和功能性的方式解决问题。但我还没有找到一个如何在没有命令性循环的情况下做到这一点的例子。

5 个答案:

答案 0 :(得分:5)

kvb的答案可能无法满足您的需求。 Seq.windowed会返回滑动值的窗口,例如[1; 2; 3; 4]变为[[1; 2; 3]; [2; 3; 4]]。看起来你想把它分成连续的块。以下函数获取列表并返回三元组列表('T list -> ('T * 'T * 'T) list)。

let toTriples list = 
  let rec aux f = function
    | a :: b :: c :: rest -> aux (fun acc -> f ((a, b, c) :: acc)) rest
    | _ -> f []
  aux id list

这是相反的:

let ofTriples triples =
  let rec aux f = function
    | (a, b, c) :: rest -> aux (fun acc -> f (a :: b :: c :: acc)) rest
    | [] -> f []
  aux id triples

修改

如果您正在处理大量数据,这里是一个基于序列的方法,使用常量内存(它创建的所有optiontuple都有对GC的负面影响 - 请参阅下面的更好版本):

let (|Next|_|) (e:IEnumerator<_>) =
  if e.MoveNext() then Some e.Current
  else None

let (|Triple|_|) = function
  | Next a & Next b & Next c -> Some (a, b, c) //change to [|a;b;c|] if you like
  | _ -> None

let toSeqTriples (items:seq<_>) =
  use e = items.GetEnumerator()
  let rec loop() =
    seq {
      match e with
      | Triple (a, b, c) -> 
        yield a, b, c
        yield! loop()
      | _ -> ()
    }
  loop()

编辑2

ebb关于内存使用的问题促使我进行测试,我发现toSeqTriples速度慢,导致GC频繁出现。以下版本修复了这些问题,几乎比基于列表的版本快4倍。

let toSeqTriplesFast (items:seq<_>) =
  use e = items.GetEnumerator()
  let rec loop() =
    seq {
      if e.MoveNext() then
        let a = e.Current
        if e.MoveNext() then 
          let b = e.Current
          if e.MoveNext() then
            let c = e.Current
            yield (a, b, c)
            yield! loop()
    }
  loop()

相对于基于列表或基于数组的方法,这具有相对恒定的内存使用率,因为a)如果你有一个seq来开始整个序列,则不必将其插入列表/数组中;并且b)它还返回一个序列,使其变得懒惰,并避免分配另一个列表/数组。

答案 1 :(得分:4)

  

我需要先将数组中的三元组分组为:

如果你知道它们将永远是三元组,那么表示为元组int * int * int比使用数组更“有类型”,因为它传达的事实是只有三个元素。

其他人已经描述了按摩数据的各种方法,但我实际上建议不要打扰(除非你的描述多于此)。我会选择一个函数来解析你的数组:

let get i = a.[3*i], a.[3*i+1], a.[3*i+2]

如果您真的想要更改表示,那么现在可以执行以下操作:

let b = Array.init (a.Length/3) get

答案真的取决于你接下来要做什么......

答案 2 :(得分:2)

更新:正如丹尼尔所指出的,这个答案是错误的,因为它会创建一个滑动窗口。

您可以使用库中的Seq.windowed功能。 E.g。

let rgbPix = rawValues |> Seq.windowed 3

这会返回一个序列而不是一个数组,所以如果你需要随机访问,你可以通过调用Seq.toArray来跟随它。

答案 3 :(得分:2)

另一种直接接受并产生数组的方法:

let splitArrays n arr =
    match Array.length arr with
    | 0 ->
        invalidArg "arr" "array is empty"
    | x when x % n <> 0 ->
        invalidArg "arr" "array length is not evenly divisible by n"
    | arrLen ->
        let ret = arrLen / n |> Array.zeroCreate
        let rec loop idx =
            ret.[idx] <- Array.sub arr (idx * n) n
            match idx + 1 with
            | idx' when idx' <> ret.Length -> loop idx'
            | _                            -> ret
        loop 0

或者,另一个:

let splitArray n arr =
    match Array.length arr with
    | 0 ->
        invalidArg "arr" "array is empty"
    | x when x % n <> 0 ->
        invalidArg "arr" "array length is not evenly divisible by n"
    | arrLen ->
        let rec loop idx = seq {
            yield Array.sub arr idx n
            let idx' = idx + n
            if idx' <> arrLen then
                yield! loop idx' }
        loop 0 |> Seq.toArray

答案 4 :(得分:2)

(帽子提示:Scott Wlaschin)从F#4.0开始,您可以使用Array.chunkBySize()。它完全符合您的要求:

let bs = [| 255;   0;   0; //Solid red
              0; 255;   0; //Solid green
              0;   0; 255; //Solid blue
              1;  72;   9; 
             34;  15; 155 |]
let grouped = bs |> Array.chunkBySize 3
// [| [|255;   0;   0|]
//    [|  0; 255;   0|]
//    [|  0;   0; 255|]
//    [|  1;  72;   9|]
//    [| 34;  15; 155|] |]

F#4.0中的ListSeq模块also have chunkBySize()。在撰写本文时,MSDN上的文档不会在任何地方显示chunkBySize(),但如果>您正在使用F#4.0,那就是