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