F # Разбить список на подсписок на основе сравнения соседних элементов

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

val split = ('T -> 'T -> bool) -> 'T list -> 'T list list

В настоящее время я пытаюсь начать с решения по требованию Дон, но я не могу понять, как инициализировать и использовать значение "prev" для сравнения. Складывается ли лучший способ?

//Don solution for single criteria, copied from hubFS
let SequencesStartingWith n (s:seq<_>) =
    seq { use ie = s.GetEnumerator()
          let acc = new ResizeArray<_>()
          while ie.MoveNext() do
             let x = ie.Current
             if x = n && acc.Count > 0 then
                 yield ResizeArray.to_list acc
                 acc.Clear()
             acc.Add x
          if acc.Count > 0 then
              yield  ResizeArray.to_list acc }

Ответ 1

Это интересная проблема! Мне нужно было реализовать именно это на С# совсем недавно для моей статьи о группировании (поскольку сигнатура типа функции очень похожа на groupBy, поэтому он может использоваться в запросе LINQ в качестве предложения group by). Однако реализация С# была довольно уродливой.

Во всяком случае, должен быть способ выразить эту функцию, используя некоторые простые примитивы. Просто кажется, что библиотека F # не предоставляет никаких функций, которые подходят для этой цели. Я смог придумать две функции, которые, как представляется, в целом полезны и могут быть объединены вместе для решения этой проблемы, так что вот они:

// Splits a list into two lists using the specified function
// The list is split between two elements for which 'f' returns 'true'
let splitAt f list =
  let rec splitAtAux acc list = 
    match list with
    | x::y::ys when f x y -> List.rev (x::acc), y::ys
    | x::xs -> splitAtAux (x::acc) xs
    | [] -> (List.rev acc), []
  splitAtAux [] list

val splitAt : ('a -> 'a -> bool) -> 'a list -> 'a list * 'a list

Это похоже на то, чего мы хотим достичь, но он разбивает список только на две части (это более простой случай, чем разбиение списка несколько раз). Затем нам нужно будет повторить эту операцию, которая может быть выполнена с помощью этой функции:

// Repeatedly uses 'f' to take several elements of the input list and
// aggregate them into value of type 'b until the remaining list 
// (second value returned by 'f') is empty
let foldUntilEmpty f list = 
  let rec foldUntilEmptyAux acc list =
    match f list with
    | l, [] -> l::acc |> List.rev
    | l, rest -> foldUntilEmptyAux (l::acc) rest
  foldUntilEmptyAux [] list

val foldUntilEmpty : ('a list -> 'b * 'a list) -> 'a list -> 'b list

Теперь мы можем повторно применить splitAt (с некоторым предикатом, указанным в качестве первого аргумента) в списке ввода, используя foldUntilEmpty, который дает нам функцию, которую мы хотели:

let splitAtEvery f list = foldUntilEmpty (splitAt f) list

splitAtEvery (<>) [ 1; 1; 1; 2; 2; 3; 3; 3; 3 ];;
val it : int list list = [[1; 1; 1]; [2; 2]; [3; 3; 3; 3]]

Я думаю, что последний шаг очень хорош:-). Первые две функции довольно просты и могут быть полезны для других вещей, хотя они не такие общие, как функции из основной библиотеки F #.

Ответ 2

Как насчет:

let splitOn test lst =
    List.foldBack (fun el lst ->
            match lst with
            | [] -> [[el]]
            | (x::xs)::ys when not (test el x) -> (el::(x::xs))::ys
            | _ -> [el]::lst
         )  lst [] 

foldBack устраняет необходимость в изменении списка.

Ответ 3

Подумав об этом немного дальше, я придумал это решение. Я не уверен, что это очень читаемо (кроме меня, кто написал это).

ОБНОВЛЕНИЕ. Основываясь на более подходящем примере в ответе Томаса, здесь улучшенная версия, которая удаляет "запах кода" (см. правки для предыдущей версии) и немного читаема (говорит мне).

Он по-прежнему разбивается на это (splitOn (<>) []) из-за ужасной ошибки ограничения значения, но я думаю, что это может быть неизбежно.

(EDIT: исправленная ошибка, отмеченная Johan Kullbom, теперь корректно работает для [1; 1; 2; 3]. Проблема заключалась в том, что два элемента были непосредственно в первом матче, это означало, что я пропустил сравнение/проверку.)

//Function for splitting list into list of lists based on comparison of adjacent elements
let splitOn test lst = 
    let rec loop lst inner outer = //inner=current sublist, outer=list of sublists
        match lst with 
        | x::y::ys when test x y -> loop (y::ys) [] (List.rev (x::inner) :: outer)
        | x::xs ->                  loop xs (x::inner) outer
        | _ ->                      List.rev ((List.rev inner) :: outer)
    loop lst [] []

splitOn (fun a b -> b - a > 1) [1]
> val it : [[1]]

splitOn (fun a b -> b - a > 1) [1;3]
> val it : [[1]; [3]]

splitOn (fun a b -> b - a > 1) [1;2;3;4;6;7;8;9;11;12;13;14;15;16;18;19;21]
> val it : [[1; 2; 3; 4]; [6; 7; 8; 9]; [11; 12; 13; 14; 15; 16]; [18; 19]; [21]]

Любые мысли об этом или частичное решение в моем вопросе?

Ответ 4

Я бы предпочел использовать List.fold по явной рекурсии.

let splitOn pred = function
    | []       -> []
    | hd :: tl -> 
        let (outer, inner, _) =
            List.fold (fun (outer, inner, prev) curr ->
                            if pred prev curr 
                            then (List.rev inner) :: outer, [curr], curr
                            else outer, curr :: inner, curr)
                      ([], [hd], hd)
                      tl
        List.rev ((List.rev inner) :: outer)

Ответ 5

"смежный" сразу заставляет меня думать о Seq.pairwise.

let splitAt pred xs =
    if Seq.isEmpty xs then
        []
    else
        xs
        |> Seq.pairwise
        |> Seq.fold (fun (curr :: rest as lists) (i, j) -> if pred i j then [j] :: lists else (j :: curr) :: rest) [[Seq.head xs]]
        |> List.rev
        |> List.map List.rev

Пример:

[1;1;2;3;3;3;2;1;2;2]
|> splitAt (>)

дает:

[[1; 1; 2; 3; 3; 3]; [2]; [1; 2; 2]]

Ответ 6

Мне нравятся ответы, предоставленные @Joh и @Johan, поскольку эти решения кажутся самыми идиоматичными и понятными. Мне также нравится идея, предложенная @Shooton. Однако у каждого решения были свои недостатки.
Я пытался избежать:

  • Реверсивные списки
  • Отмена и объединение временных результатов
  • Сложные инструкции match
  • Даже Seq.pairwise оказался избыточным
  • Список проверок для пустоты можно снять за счет использования Unchecked.defaultof<_> ниже

Здесь моя версия:

let splitWhen f src =
    if List.isEmpty src then [] else
    src
    |> List.foldBack
        (fun el (prev, current, rest) ->
            if f el prev
            then el , [el]          , current :: rest
            else el , el :: current , rest
        )
        <| (List.head src, [], [])               // Initial value does not matter, dislike using Unchecked.defaultof<_>
    |> fun (_, current, rest) -> current :: rest // Merge temporary lists
    |> List.filter (not << List.isEmpty)         // Drop tail element