Есть ли в 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 ответов

Решение

Ответ 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

РЕДАКТИРОВАТЬ

Если вы имеете дело с огромными объемами данных, вот последовательный подход с постоянным использованием памяти (все option с и tuple s это создает негативное влияние на 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

Вопрос Эбба об использовании памяти побудил меня проверить, и я нашел 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()

Это имеет относительно постоянное использование памяти по сравнению со списком или подходом на основе массива, потому что а) если у вас есть seq начинать со всей последовательности не нужно втирать в список / массив; и b) он также возвращает последовательность, делая ее ленивой и избегая выделения еще одного списка / массива.

Мне нужно сначала сгруппировать тройки в массиве примерно так:

Если вы знаете, что они всегда будут тройными, то представляйте их как кортежи 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

Ответ действительно зависит от того, что вы хотите сделать дальше, хотя...

(Шляпа: Скотт Влашин) Начиная с 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|] |]

List а также Seq модули также имеют chunkBySize() в F# 4.0. На момент написания статьи документы MSDN не отображались chunkBySize() где угодно, но он есть, если вы используете F# 4.0.

ОБНОВЛЕНИЕ: Как указал Даниэль, этот ответ неверен, потому что он создает скользящее окно.

Вы можете использовать Seq.windowed функция из библиотеки. Например

let rgbPix = rawValues |> Seq.windowed 3

Это возвращает последовательность, а не массив, поэтому, если вам нужен произвольный доступ, вы можете выполнить это с помощью вызова Seq.toArray,

Другой подход, который принимает и дает массивы напрямую:

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
Другие вопросы по тегам