reconbot / streaming-iterables

A Swiss army knife for async iterables. Designed to replace your streams.
https://www.npmjs.com/package/streaming-iterables
MIT License
79 stars 8 forks source link

feature suggestion: takeWhile() #275

Open wellcaffeinated opened 2 years ago

wellcaffeinated commented 2 years ago

hey there. Great library!

A takeWhile() operator would be great.

Something like this?

const takeWhile = (predicate, inclusive = false) => async function* (iterable){
  for await (const res of iterable) {
    if (!await predicate(res)){
      if (inclusive){ yield res }
      return
    }
    yield res
  }
}
const count = [1, 2, 3, 4, 5, 6, 7]
await collect(takeWhile(x => x < 5)(count))
// => [1, 2, 3, 4]
reconbot commented 2 years ago

Would this be the same as filter?

And thank you 😊

wellcaffeinated commented 2 years ago

I don't think it's the same as filter because this would terminate the stream early, allowing for early collection, or ability to exit out of a find algorithm.

For example:

This could exit early once charmander is found.

const pm = await pipeline(
  getPokemon(),
  takeWhile(pokemon => pokemon.name !== 'charmander', true),
  take(-1), // this would also be great to implement btw
  collect
)
// => pm[0].name === 'charmander'

This would have to cycle through all 720 pokemon before exiting

const pm = await pipeline(
  getPokemon(),
  filter(pokemon => pokemon.name === 'charmander'),
  collect
)

Generally i think implementing some of the methods that rust's Iterator has would be great: https://doc.rust-lang.org/core/iter/trait.Iterator.html

Or perhaps looking through the list of operators that rxjs provides: https://rxjs.dev/api?type=function

reconbot commented 2 years ago

Well stated, thank you. Yeah this makes a lot of sense. I'd happily take a pr if you're interested.

I'll dig into the rust docs, but I'm also happy to take anything useful.

reconbot commented 2 years ago

What would take(-1) do? Pulling from the end of an iterable is tough when it's async, requires buffering some or all of the data depending on the operation. I've purposely not put anything in that would accidentally cause a huge performance issue.

wellcaffeinated commented 2 years ago

Absolutely, i don't think you'd want to buffer much. I'm imagining that it would effectively release the data it doesn't need. Maybe a take(-n) could look like:

const takeRight = (count) => async function* (iterable){
  const buffer = []
  for await (const res of iterable) {
     buffer.push(res)
     if (buffer.length > count){ buffer.shift() }
  }
  while (buffer.length){ yield buffer.pop() }
}

const take = (n) => {
   if (n < 0) { return takeRight(-n) }
   else { return take(n) }
}

I'd be happy to submit some pull requests