fsprojects / FSharp.Control.TaskSeq

A computation expression and module for seamless working with IAsyncEnumerable<'T> as if it is just another sequence
MIT License
91 stars 7 forks source link

Implement `TaskSeq.skipWhile`, `skipWhileAsync`, `skipWhileInclusive` and `skipWhileInclusiveAsync` #219

Closed abelbraaksma closed 8 months ago

abelbraaksma commented 8 months ago

Fixes #130 (edit: see also #235)

Part of the push for implementing low-hanging from as listed in #208. Implements the following signatures:

static member skipWhile: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T>
static member skipWhileAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> TaskSeq<'T>
static member skipWhileInclusive: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T>
static member skipWhileInclusiveAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> TaskSeq<'T>

Note that inclusive here means that it skips at least one item (i.e., the modifier applies to the action of skipping). This is analogous to how takeWhileInclusive etc work.

Some explanation may be warranted for future reference. Here's a truth table of 1..10 and a filter function of filter x = x < 5 for both inclusive and non-inclusive skip functions (also applies to take functions):

skipWhile

// truth table for f(x) = x < 5
// 1 2 3 4 5 6 7 8 9 10
// T T T T F F F F F F (stops at first F)
// x x x x _ _ _ _ _ _ (skips exclusive)

> [1..10] |> TaskSeq.ofList |> TaskSeq.skipWhile (fun x -> x < 5);;
val it: TaskSeq<int> = taskSeq [5; 6; 7; 8; 9; 10]

skipWhileInclusive

// truth table for f(x) = x < 5
// 1 2 3 4 5 6 7 8 9 10
// T T T T F F F F F F (stops at first F)
// x x x x x _ _ _ _ _ (skips inclusive)

> [1..10] |> TaskSeq.ofList |> TaskSeq.skipWhile (fun x -> x < 5);;
val it: TaskSeq<int> = taskSeq [6; 7; 8; 9; 10]

TODO list:

abelbraaksma commented 8 months ago

@bartelink, in case you have some time, I can use an extra pair of eyes here (meanwhile, I'll fix the failing tests).

abelbraaksma commented 8 months ago

@bartelink thanks for the extensive review, much appreciated! I applied all your comments, insofar they were still applicable after some other changes.

abelbraaksma commented 8 months ago

Almost forgot to update the readmes and release notes, oops!

bartelink commented 8 months ago

re that use case... there's a double iteration in the error case and its actually the last item returned that would be the failing one but if there was only one item in the list then that last item would be valid

the example you have feels more like you are picturing a scan until or something

if you want to skip while some condition and then skip one more, that's not going to help you show the extra one you skipped

is there some partition function of some kind that this might make sense as? such a thing could split a list into 3 over some pivot or something?

bartelink commented 8 months ago

for takeWhile, I can explain the use case more easily...

I am searching until I find something if I reach the end of the list, then that's fine too but I also want the final item that I was searching for so its a 'scanUntil' semantic, which AsyncSeq/Eirik happened to call takeWhileInclusive as its literally TakeWhile but includes the last item

It does not particularly lend itself to showing the failing one, as the final item can either simply be the last item in the list or it can be the item that defines the place where we abort the search

In completely concrete terms, I'm searching backward through a list of events until I meet either a) a Snapshot b) a Reset event or c) the end of the list Once I have that part of the list, I can fold/reduce from there forward

abelbraaksma commented 8 months ago

Oh yeah, I deleted my "use case" shortly after posting it. But here's one:

Yep, a bit contrived, for sure. I'm sure other use cases will come up, or not...

bartelink commented 8 months ago

so you have a list with pairs of items and when you see the first one, you want to skip its partner that's not very general though - what if the pairs that are not pairs that you are explaining as pairs were triples?

ultimately the reason why takeWhileInclusive does make sense is that it's all about the single item being tested - do you want the final one or not

also not wanting the one after that you're not even going to look at before discarding implicitly is not a logical inverse of that

(See above about windowing/partitioning/having a pivot item - SkipWhileInclusive is ultimately very baroque and I'm not seeing any real example - maybe if you can find one in the wild (or any prior art / discussion of this logical duality), that would help?)

abelbraaksma commented 8 months ago

maybe if you can find one in the wild (or any prior art / discussion of this logical duality), that would help?)

Yeah, that's gonna be hard. Just like with takeWhileInclusive, which I cannot find "in the wild" on a quick search, I doubt I can find its logical inverse, skipWhileInclusive. Let's keep it in, as not doing so would raise questions on takeWhileInclusive. If we want the one, we should want the other as well (parity, orthogonality, principle of least surprise etc).

abelbraaksma commented 8 months ago

Some things I missed from review comments go in #220.