Последовательность в F# складывающихся троек

Я гуглил и читал, и я пытаюсь найти "правильный" способ сделать это, но каждый вопрос, который я читаю на SO, кажется, имеет совершенно разные ответы.

Вот суть моей проблемы. У файлов есть сигнатура типа seq тройки (a: строка, b: строка,c:Int64). Будучи новичком в F#, я до сих пор не свободно выражаю подписи типов (или, если на то пошло, понимаю их). a является именем файла, b является внутренним идентификатором, а c является значением, представляющим длину (размер) файла. baseconfig - это строка из предыдущего кода.

ignore(files 
    |> Seq.filter( fun(x,y,z) ->  y = baseconfig)  // used to filter only files we want
    |> Seq.fold( fun f n   -> 
        if( (fun (_,_,z) -> z) n > 50L*1024L*1024L) then
            zipfilex.Add((fun (z:string, _, _) -> z) n)
            printfn("Adding 50mb to zip")
            zipfilex.CommitUpdate()
            zipfilex.BeginUpdate()
            ("","",0L)
        else
            zipfilex.Add((fun (z, _, _) -> z) n)
            ("", "", (fun (_, _, z:Int64) -> z) n + (fun (_, _, z:Int64) -> z) f)
    ) ("","",0L)
    )

Что должен делать этот кусок кода, так это перебирать каждый файл в files добавьте его в zip-архив (но не совсем, он просто попадет в список для последующей фиксации), а когда файлы превысят 50 МБ, зафиксируйте ожидающие файлы в zip-архиве. Добавление файла - это дешево, фиксация - это дорого, поэтому я стараюсь снизить стоимость, группируя его.

Пока что код вроде работает... За исключением исключения ObjectDisposedException, которое я получил, когда оно приблизилось к 150 МБ зафиксированных файлов. Но я не уверен, что это правильный способ сделать такую ​​операцию. Такое ощущение, что я использую Seq.fold нетрадиционным способом, но пока я не знаю лучшего способа сделать это.

Бонусный вопрос: есть ли лучший способ вырезать значения из кортежей? fst и snd работают только для двухзначных кортежей, и я понимаю, что вы можете определять свои собственные функции вместо встроенных, как я, но кажется, что должен быть лучший способ.

Обновление: Мои предыдущие попытки сбрасывать, я не мог понять, почему я не мог просто использовать Int64 в качестве аккумулятора. Оказывается, мне не хватало критических скобок. Немного более простая версия ниже. Также устраняет все сумасшедшие извлечения кортежей.

ignore(foundoldfiles 
    |> Seq.filter( fun (x,y,z) ->  y = baseconfig) 
    |> Seq.fold( fun (a) (f,g,j)   -> 
        zipfilex.Add( f)
        if( a > 50L*1024L*1024L) then
            printfn("Adding 50mb to zip")
            zipfilex.CommitUpdate()
            zipfilex.BeginUpdate()
            0L
        else
             a + j
    ) 0L
    )

Обновление 2: мне придется пойти с императивным решением, F# каким-то образом повторно вводит этот блок кода, после того, как zip-файл закрыт в последующем утверждении. Что объясняет исключение ObjectDisposedException. Понятия не имею, как это работает и почему.

4 ответа

Решение

Я не думаю, что ваша проблема выигрывает от использования fold, Это наиболее полезно при построении неизменных структур. Мое мнение в данном случае таково, что это делает то, что вы пытаетесь сделать, менее понятным. Императивное решение работает хорошо:

let mutable a = 0L
for (f, g, j) in foundoldfiles do
    if g = baseconfig then
        zipfilex.Add(f)
        if a > 50L * 1024L * 1024L then
            printfn "Adding 50mb to zip"
            zipfilex.CommitUpdate()
            zipfilex.BeginUpdate()
            a <- 0L
        else
            a <- a + j

В качестве альтернативы "грязному" императивному стилю вы можете продлить Seq модуль с общей и многоразовой функцией для чанкинга. Функция немного похожа fold, но требуется лямбда, которая возвращает option<'State>, Если он вернется Noneзатем запускается новый блок, а в противном случае элемент добавляется к предыдущему фрагменту. Тогда вы можете написать элегантное решение:

files
|> Seq.filter(fun (x, y, z) ->  y = baseconfig) 
|> Seq.chunkBy(fun (x, y, z) sum -> 
     if sum + z > 50L*1024L*1024L then None
     else Some(sum + z)) 0L
|> Seq.iter(fun files ->
    zipfilex.BeginUpdate()
    for f, _, _ in files do zipfilex.Add(f)
    zipfilex.CommitUpdate())

Реализация chunkBy функция немного длиннее - ее нужно использовать IEnumerator непосредственно и это можно выразить с помощью рекурсии:

module Seq = 
  let chunkBy f initst (files:seq<_>) = 
    let en = files.GetEnumerator()
    let rec loop chunk st = seq {
      if not (en.MoveNext()) then
        if chunk <> [] then yield chunk
      else
        match f en.Current st with
        | Some(nst) -> yield! loop (en.Current::chunk) nst
        | None -> 
            yield chunk 
            yield! loop [en.Current] initst }
    loop [] initst

Если вам не нравятся изменяемые переменные и императивные циклы, вы всегда можете переписать это, используя GOTO функциональный цикл:

let rec loop acc = function
    | (file, id, size) :: files ->
        if id = baseconfig then
            zipfilex.Add file
            if acc > 50L*1024L*1024L then
                printfn "Adding 50mb to zip"
                zipfilex.CommitUpdate()
                zipfilex.BeginUpdate()
                loop 0L files
            else
                loop (acc + size) files
        else
            loop acc files
    | [] -> ()

loop 0L foundoldfiles

Преимущество этого в том, что он явно указывает три различных способа, которыми может развиваться индуктивный случай, и как преобразователь аккумулятора в каждом случае (так что вы вряд ли поймете это неправильно - посмотрите на ошибку в цикле Дэниела).

Вы даже можете переместить проверку baseconfig в предложение when:

let rec loop acc = function
    | (file, id, size) :: files when id = baseconfig ->
        zipfilex.Add file
        if acc > 50L*1024L*1024L then
            printfn "Adding 50mb to zip"
            zipfilex.CommitUpdate()
            zipfilex.BeginUpdate()
            loop 0L files
        else
            loop (acc + size) files
    | _ :: files -> loop acc files
    | [] -> ()

loop 0L foundoldfiles

Вот мое мнение:

let inline zip a b = a, b

foundoldfiles 
|> Seq.filter (fun (_, internalid, _) -> internalid = baseconfig)
|> zip 0L
||> Seq.fold (fun acc (filename, _, filesize) -> 
    zipfilex.Add filename
    let acc = acc + filesize
    if acc > 50L*1024L*1024L then
        printfn "Adding 50mb to zip"
        zipfilex.CommitUpdate ()
        zipfilex.BeginUpdate ()
        0L
    else acc)
|> ignore

Некоторые заметки:

  • zip вспомогательная функция обеспечивает очистку конвейера через всю функцию без каких-либо накладных расходов, а в более сложных сценариях помогает с выводом типа, так как состояние смещается справа налево от fold функтор (хотя это не имеет значения или помогает в данном конкретном случае)
  • Использование _ локальное удаление ненужных элементов кортежа облегчает чтение кода
  • Подход конвейерной в ignore вместо переноса всего выражения с дополнительными скобками код легче читать
  • Заключение аргументов унарных функций в круглые скобки выглядит странно; Вы не можете использовать скобки для не унарных функций карри, поэтому использование их для унарных функций противоречиво. Моя политика заключается в том, чтобы зарезервировать круглые скобки для вызовов конструктора и вызовов tupled-функций

РЕДАКТИРОВАТЬ: PS if( a > 50L*1024L*1024L) then неверная логика - if Необходимо учитывать аккумулятор плюс текущий размер файла. Например, если первый файл был>= 50 МБ, тогда if не сработал бы.

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