Самый идиоматичный способ написания пакетов размера seq в F#

Я пытаюсь выучить F#, переписав некоторые алгоритмы C#, которые у меня есть, в идиоматический F#.

Одной из первых функций, которые я пытаюсь переписать, является пакет, где:

[1..17] |> batchesOf 5

Который разделил бы последовательность на партии с максимум пятью в каждой, то есть:

[[1; 2; 3; 4; 5]; [6; 7; 8; 9; 10]; [11; 12; 13; 14; 15]; [16; 17]]

Моя первая попытка сделать это довольно уродливо, когда я прибегнул к использованию изменяемого объекта ref после того, как столкнулся с ошибками, пытаясь использовать изменяемый тип внутри замыкания. Использование ref особенно неприятно, поскольку для разыменования его необходимо использовать ! Оператор, который, находясь внутри выражения условия, может быть противоречащим интуиции для некоторых разработчиков, которые не будут считать его логическим. Другая проблема, с которой я столкнулся, заключается в том, что Seq.skip и Seq.take не похожи на псевдонимы Linq, поскольку выдают ошибку, если размер превышает размер последовательности.

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#? Сохранение исходного поведения, но желательно без изменяемой переменной.

10 ответов

Решение

Реализация этой функции с помощью seq<_> Тип идиоматически сложен - тип по своей природе изменчив, поэтому простого функционального способа просто не существует. Ваша версия довольно неэффективна, потому что она использует Skip неоднократно в последовательности. Лучший императивный вариант будет использовать GetEnumerator и просто перебирать элементы, используя IEnumerator, Вы можете найти различные императивные варианты в этом фрагменте: 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, но это не стандартная и довольно продвинутая уловка (например, см. http://fssnip.net/37).

Друг спросил меня об этом некоторое время назад. Вот переработанный ответ. Это работает и чисто:

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

Надеюсь, это поможет!

Ура, мы можем использовать List.chunkBySize, Seq.chunkBySize а также Array.chunkBySize в F# 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#, хотя

Я нашел это довольно кратким решением:

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 элементов из входной последовательности.

Мой метод включает в себя преобразование списка в массив и рекурсивное разбиение массива:

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

Вот простая реализация для последовательностей:

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

Это не возможно идиоматично, но это работает:

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

Эта версия проходит все мои тесты, которые я мог придумать, включая тесты для отложенной оценки и оценки одной последовательности:

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#, поэтому, если я что-то упустил - пожалуйста, исправьте меня, это будет очень цениться.

Вы можете решить свою задачу с помощью аналога Clojure partition Функция библиотеки ниже:

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 Вы можете использовать его для нарезки перекрывающихся пакетов, таких как скользящие окна, и даже применять к бесконечным последовательностям, как показано ниже:

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

Рассматривайте его только как прототип, поскольку он выполняет много избыточных оценок исходной последовательности и вряд ли подходит для производственных целей.

Другие вопросы по тегам