как сделать Seq.takeWhile + один элемент в F#

Я хотел бы написать функцию, которая фильтрует последовательность с помощью предиката, но результат также должен ВКЛЮЧАТЬ первый элемент, для которого предикат возвращает false.

Логика была бы примерно такой, если бы в F# было ключевое слово break

let myFilter predicate s =
    seq {
        for item in s do
            yield item
            if predicate item then
                break
    }

Я пробовал комбинации Seq.takeWhile и Seq.skipWhile, что-то вроде этого:

Seq.append 
    (Seq.takeWhile predicate s) 
    (Seq.skipWhile predicate s |> Seq.take 1)

... но проблема в том, что первый элемент, который соответствует предикату, теряется между takeWhile и skipWhile

Также обратите внимание, что входная последовательность является ленивой, поэтому любое решение, которое использует последовательность и принимает решения впоследствии, нежизнеспособно.

Любые идеи?

Спасибо!

РЕДАКТИРОВАТЬ: Большое спасибо за все ответы! Я не ожидал, что так много ответов так быстро. Я рассмотрю каждый из них в ближайшее время. Теперь я просто хочу дать немного больше контекста. Рассмотрим следующий ката код, который реализует оболочку:

let cmdProcessor state = function
    | "q" -> "Good bye!"
    | "h" -> "Help content"
    | c -> sprintf "Bad command: '%s'" c

let processUntilQuit =
    Seq.takeWhile (fun cmd -> cmd <> "q")

let processor = 
    processUntilQuit
    >> Seq.scan cmdProcessor "Welcome!"

module io =
    let consoleLines = seq { while true do yield System.Console.ReadLine () }

    let display : string seq -> unit = Seq.iter <| printfn "%s" 

io.consoleLines |> processor|> io.display

printf "Press any key to continue..."
System.Console.ReadKey ()|> ignore

Проблема этой реализации в том, что она не печатает "До свидания!" при вводе команды q.

Что я хочу сделать, так это реализовать функцию processUntilQuit так, чтобы она обрабатывала все команды до "q", включая "q".


person vidi    schedule 24.09.2012    source источник
comment
Что вы подразумеваете под проблемой в том, что первый элемент, соответствующий предикату, теряется между takeWhile и skipWhile? Ваше решение верно, потому что первый ложный элемент все еще находится в s.   -  person pad    schedule 24.09.2012
comment
@pad: Итак, если я реализую функцию processUntilQuit следующим образом: let processUntilQuit cmds = let isNotFinished cmd = cmd ‹› q seq {yield! Seq.takeWhile isNotFinished дает результат команд! Seq.skipWhile isNotFinished cmds |› Seq.truncate 1} ...вывод выглядит следующим образом: Добро пожаловать! д д До свидания! Нажмите любую клавишу, чтобы продолжить... Как видите, мне пришлось дважды нажать q, чтобы выйти.   -  person vidi    schedule 24.09.2012


Ответы (11)


Отсутствие поддержки break в вычислительных выражениях немного раздражает. Он плохо согласуется с моделью, используемой в F# (поэтому и не поддерживается), но в данном случае был бы очень полезен.

Если вы хотите реализовать это, используя только одну итерацию последовательности, то я думаю, что самое чистое решение — просто использовать базовую структуру последовательностей и записать ее как рекурсивный цикл, используя IEnumerator<'T>

Это довольно короткий (по сравнению с другими решениями здесь) и довольно понятный код:

let myFilter predicate (s:seq<_>) = 
  /// Iterates over the enumerator, yielding elements and
  /// stops after an element for which the predicate does not hold
  let rec loop (en:IEnumerator<_>) = seq {
    if en.MoveNext() then
      // Always yield the current, stop if predicate does not hold
      yield en.Current
      if predicate en.Current then
        yield! loop en }

  // Get enumerator of the sequence and yield all results
  // (making sure that the enumerator gets disposed)
  seq { use en = s.GetEnumerator()
        yield! loop en }
person Tomas Petricek    schedule 24.09.2012
comment
Это не совсем функционально, но работает и достаточно чисто. Спасибо! Все равно было бы интересно посмотреть, как это можно написать красивым функциональным способом. - person vidi; 24.09.2012
comment
У меня нет доказательств для этого утверждения, но я думаю, что оно не может быть написано хорошим функциональным способом, потому что базовая структура данных - seq<'T> на самом деле не функциональна. Вы могли бы написать его в функциональном стиле, если бы использовали ленивый список, но последовательности более идиоматичны в F# (даже если они иногда вынуждают вас использовать императивный стиль для реализации некоторой декларативной функции более высокого уровня). - person Tomas Petricek; 24.09.2012

Не совсем понимаю, в чем проблема с вашим решением.

Две небольшие поправки:

(1) Используйте выражение последовательности для удобочитаемости.

(2) Используйте Seq.truncate вместо Seq.take, если входная последовательность пуста.

let myFilter predicate s = 
    seq { yield! Seq.takeWhile predicate s
          yield! s |> Seq.skipWhile predicate |> Seq.truncate 1 }
person pad    schedule 24.09.2012
comment
Разве он не будет перечислять s дважды в худшем случае (все предикаты совпадения s)? - person rkrahl; 24.09.2012
comment
Да, это будет. Но я не думаю, что вопрос касается ОП. - person pad; 24.09.2012
comment
Я не понимаю вашего первого пункта - я думаю, что и версии OP, и ваши версии одинаково ленивы. Я бы тоже так написал, но это просто стилистическое предпочтение... (Хорошее замечание по поводу truncate.) - person Tomas Petricek; 24.09.2012

let duplicateHead xs = seq { yield Seq.head xs; yield! xs }
let filter predicate xs =
    xs
    |> duplicateHead
    |> Seq.pairwise
    |> Seq.takeWhile (fst >> predicate)
    |> Seq.map snd

Альтернативная версия duplicateHead, на случай, если вам не нравится выражение вычисления здесь:

let duplicateHead' xs =
    Seq.append 
        (Seq.head xs)
        xs

Этот подход основан на построении кортежей текущего и следующего элементов. predicate применяется к текущему элементу, но возвращается следующий.

ПРИМЕЧАНИЕ. Это не безопасно для случаев, когда predicate не работает на самом первом элементе. Чтобы заставить его работать нормально, вам нужно переработать duplicateHead, добавив элемент, который обязательно передаст predicate.

person bytebuster    schedule 24.09.2012
comment
Это почти работает, но читает еще одну команду после q и, как вы уже сказали, все еще есть проблема, если первая команда q - person vidi; 24.09.2012

Еще один поздний ответ, но он «функциональный», простой и не читает никаких элементов после последнего в последовательности результатов.

let myFilter predicate =
    Seq.collect (fun x -> [Choice1Of2 x; Choice2Of2 (predicate x)])
    >> Seq.takeWhile (function | Choice1Of2 _ -> true | Choice2Of2 p -> p)
    >> Seq.choose (function | Choice1Of2 x -> Some x | Choice2Of2 _ -> None)
person Max Kiselev    schedule 03.07.2019

Уродливое нефункциональное решение

let myfilter f s =
    let failed = ref false
    let newf = fun elem -> match !failed with 
                           |true -> 
                               failed := f elem
                               true
                           |false->false
    Seq.takeWhile newf s
person John Palmer    schedule 24.09.2012
comment
С этой реализацией после того, как я ввел команду q, она читает еще одну команду, а затем выходит - person vidi; 24.09.2012

Немного лучше. :)

let padWithTrue n xs = seq { for _ in 1..n do yield true; done; yield! xs }
let filter predicate n xs =
    let ys = xs |> Seq.map predicate |> padWithTrue n
    Seq.zip xs ys
    |> Seq.takeWhile snd
    |> Seq.map fst

Он принимает дополнительный параметр n, который определяет, сколько дополнительных элементов добавить.

ПРИМЕЧАНИЕ: будьте осторожны с однострочными padWithTrue (ключевое слово done)

person bytebuster    schedule 24.09.2012
comment
Этот не работает. Ему нужны команды, введенные дважды. Это реализация, которую я написал на основе вашего предложения: let processUntilQuit cmds = let padWithTrue n xs = seq { for _ in 1..n do yield true; сделано; урожай! xs } let ys = cmds |› Seq.map (fun cmd -> cmd ‹› q) |› padWithTrue 1 Seq.zip cmds ys |› Seq.takeWhile snd |› Seq.map fst - person vidi; 24.09.2012

Уродливое функциональное решение :):

let rec myFilter predicate =
        Seq.fold (fun acc s ->
            match acc with
                | (Some x, fs) -> 
                    match predicate s with
                        | true -> (Some x, fs @ [s])
                        | false -> (Some x, fs)
                | (_, fs) ->
                    match predicate s with
                        | true -> (None, fs @ [s])
                        | false -> (Some s, fs))
            (None, [])

Вы получаете кортеж, первый элемент которого содержит вариант с первым несовпадающим элементом из исходного списка, а второй элемент содержит отфильтрованный список.

Уродливое функциональное ленивое решение (извините, я не прочитал ваш пост правильно в первый раз):

let myFilterLazy predicate s =
        let rec inner x =
            seq {
                match x with
                    | (true, ss) when ss |> Seq.isEmpty = false ->
                        let y = ss |> Seq.head
                        if predicate y = true then yield y
                        yield! inner (true, ss |> Seq.skip 1)
                    | (_, ss) when ss |> Seq.isEmpty = false ->
                        let y = ss |> Seq.head
                        if predicate y = true then
                            yield y
                            yield! inner (false, ss |> Seq.skip 1)
                        else
                            yield y
                            yield! inner (true, ss |> Seq.skip 1)
                    | _ -> 0.0 |> ignore
            }

        inner (false, s)

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

Редактировать: Не такое уж уродливое, чистое решение F#, вдохновленное ответом Томаса Петричека:

let myFilterLazy2 predicate s =
        let rec inner ss = seq {
            if Seq.isEmpty ss = false then
                yield ss |> Seq.head
                if ss |> Seq.head |> predicate then
                    yield! ss |> Seq.skip 1 |> inner
        }

        inner s
person rkrahl    schedule 24.09.2012
comment
Это заслуживает +1 за самое уродливое решение :) Спасибо, но я надеялся на что-то более чистое. - person vidi; 24.09.2012
comment
Спасибо за оценку :). Третья версия, чистый F# без странных классов .net ;). Вдохновленный ответом Томаса Петричека, который также является моим любимым. - person rkrahl; 24.09.2012
comment
Я только что попробовал ваше второе решение и не работает. Ему нужны команды, введенные дважды. - person vidi; 25.09.2012

Я догадываюсь, что вы хотите, чтобы он принял:

let takeUntil pred s =
  let state = ref true
  Seq.takeWhile (fun el ->
    let ret= !state
    state := not <| pred el
    ret
    ) s
person Mike Kowalski    schedule 26.07.2014
comment
Это неверно, так как он оценивает один дополнительный элемент (который может иметь нежелательный побочный эффект). - person brianberns; 03.05.2015

Это очень старо, но я подумал, что внесу свой вклад, потому что другие решения не предлагали этого...

Как насчет того, чтобы использовать Seq.scan для создания двухэлементного стека результатов предиката и просто взять, в то время как нижняя часть этого стека, представляющая результат предиката предыдущего элемента, равна true? (обратите внимание, не тестировал этот код)

Seq.scan (fun (a,b,v) e -> (pred e, a, Some e)) (true, true, None )
>> Seq.takeWhile (fun (_,b,_) -> b)
>> Seq.map (fun (_,_,c) -> c)
person George    schedule 14.11.2017

Я понимаю, что это старый вопрос. Но есть более функциональное решение.

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

let takeWhileAndNext predicate mySequence =
    let folder pred state element =
        match state with
            | Some (_, false) ->
                None
            | _ ->
                Some (Some element, pred element)
    let initialState = Some (None, true)
    Seq.scan (folder predicate) initialState mySequence |> Seq.takeWhile Option.isSome
                                                        |> Seq.map Option.get
                                                        |> Seq.map fst
                                                        |> Seq.filter Option.isSome
                                                        |> Seq.map Option.get

В предпоследней строке |> Seq.filter Option.isSome можно заменить на |> Seq.tail, так как никакие другие состояния, кроме initialState, не соответствуют Some (None, _).

person Adhemar    schedule 18.12.2017

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

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

/// allows fetching elements from same sequence
type ContinueSequence<'a> (xs: 'a seq) =

    let en = xs.GetEnumerator()

    member _.Continue (includeCurrent: bool) =
        let s = seq { while en.MoveNext() do yield en.Current }
        let c = seq { en.Current }
        if includeCurrent then
            Seq.append c s
        else
            s

    interface IDisposable with 
        member _.Dispose() =
            en.Dispose()

Фактический ответ на вопрос будет:

use seq = new ContinueSequence a 
let result = Seq.append
   seq.Continue(false) |> Seq.takeWhile(predicate) 
   seq.Continue(true) |> Seq.take(1)  //include element which breaks predicate

Более общий пример

/// usage example:
let a = seq [1; 2; 3; 4; 5; 6; 7]

use seq = new ContinueSequence<_>(a)
let s1 = seq.Continue(false) |> Seq.takeWhile((>) 3) // take 1 and 2, 3 is current
let s2 = seq.Continue(true) |> Seq.take(2)    // take 3 and 4
let s3 = seq.Continue(false) |> Seq.skip(1)   // skip 5

let s = 
    s1 
    |> Seq.append <| s2 
    |> Seq.append <| s3 
    |> Seq.toList

// s = [1; 2; 3; 4; 6; 7]
person irriss    schedule 26.04.2020