-
Couldn't load subscription status.
- Fork 1.4k
Add ZStream#interleave, ZStream#interleaveWith, ZStream#combine #1355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adamgfraser Great work. This looks like a great combinator.
The only thought I have is that the implementations of zipWith, mergeWith and this combinator are all eerily similar. I wonder if we can abstract over them? @jdegoes could you have a look at this?
| * either `s1` or `s2` are exhausted further requests for values from them | ||
| * will be ignored. | ||
| */ | ||
| final def interleave[R, E, A]( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about an infix version too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely.
| rightQueue <- s2.toQueue() | ||
| } yield (queue, leftQueue, rightQueue) | ||
|
|
||
| ZManaged.fromEffect { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I think the scoping is off here. ZManaged#use is usually not needed in stream combinators, unless you are sure you want the finalizers to fire. Something like this would defer them until the caller decides they should run:
for {
queue <- b.toQueue
leftQueue <- s1.toQueue
rightQueue <- s2.toQueue
result <- loop(false, false, s, queue, leftQueue, rightQueue).toManaged_
} yield resultThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that works. I see now that it doesn't make sense to use a resource and then just wrap it in another resource.
| * will be ignored. | ||
| */ | ||
| final def interleave[R, E, A]( | ||
| b: ZStream[R, E, Boolean], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this expressive enough to handle two streams with different lengths? if all the caller of this combinator can do is provide a stream of booleans, surely they can't know if one of the streams ended and therefore they will not be able to "switch over" to reading just one side.
Maybe we need this to be a function that uses the queues themselves?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking that if the stream of booleans eventually requested values from both side like Stream(true, false).forever then the caller would be able to get all the values, but it is a little indirect.
What if we added a parameter f: (ZStream[R, E, A], ZStream[R, E, A]) => ZStream[R, E, A) where the first stream is the result of interleaving and the second is the "leftover" stream? So if the caller wanted to pull all remaining elements from the leftover stream they could use _ ++ _, if they wants to ignore to ignore them they could use _._1, or they could apply more complex logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I read more carefully and saw that you skip over a side that has ended. That makes sense! I think this is fine in this form then.
I suggest that we have a shorthand combinator for the common case of Stream(false, true).forever though :-) This combinator can be called interleaveWith and the shorter one interleave.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree.
Perhaps a unification might look something like: |
|
Right, I agree with that direction. What do you think @adamgfraser? Want to give this a shot? |
|
@iravid Yes, I'm on it! |
| left <- s1.toQueue(lc) | ||
| right <- s2.toQueue(rc) | ||
| dest <- Queue.unbounded[Take[E, C]].toManaged(_.shutdown) | ||
| _ <- f0(left, right, dest).toManaged_ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we need to fork here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually need an output queue? I haven't tested if all combinators work with this, but what if we do the following:
f0: (Queue[Take[E, A]], Queue[Take[E, B]]) => ZIO[R, E, Take[E, C]]- run
f0withZStream.unfoldMandunTake
Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me give it a try.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks excellent! See my comment on mergeWith. We can either enhance combine further with a state parameter or defer the enhancement and the mergeWith re-implementation - your call!
| result <- ZStream | ||
| .unfoldM((left, right)) { | ||
| case (left, right) => | ||
| f0(left, right).map(take => Some((take, (left, right)))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah we could actually drop the unTake and signal errors/completion here directly. Take.option can help here.
| } | ||
| for { | ||
| queue <- ZStream.managed(queue) | ||
| result <- ZStream.combine(self, that)((_, _) => queue.take) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😂
I think this could be implemented if ZStream.combine allowed the function to keep a state between the invocations, like an unfold. Then you could use raceWith to race takes against the two queues. Whenever one side wins, you pass the pending side through the state to the next invocation. That would save the need for the output queue here.
That said, we can defer that to a follow-up ticket that enhances combine and implements mergeWith in terms of it if you'd like to get interleave in sooner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think that makes sense. I guess we could potentially use a Ref to pass the loser from one invocation to another like I did to manage state in the other combinators. But I was definitely feeling the lack of ability to maintain some state in writing them. It would probably be both easier to use and more performant to pass the state explicitly.
| right <- that.toQueue[E2, B](rc) | ||
| s <- ZManaged.fromEffect(loop(false, false, left, right, s)) | ||
| } yield s | ||
| ): ZStream[R1, E1, C] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice code removal here!
| * will be ignored. | ||
| */ | ||
| final def interleave[R, E, A]( | ||
| b: ZStream[R, E, Boolean], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I read more carefully and saw that you skip over a side that has ended. That makes sense! I think this is fine in this form then.
I suggest that we have a shorthand combinator for the common case of Stream(false, true).forever though :-) This combinator can be called interleaveWith and the shorter one interleave.
| final def bracket[R, E, A](acquire: ZIO[R, E, A])(release: A => ZIO[R, Nothing, _]): ZStream[R, E, A] = | ||
| managed(ZManaged.make(acquire)(release)) | ||
|
|
||
| final def combine[R, E, A, B, C](s1: ZStream[R, E, A], s2: ZStream[R, E, B], lc: Int = 2, rc: Int = 2)( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one could also use an infix version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also interesting to think of how would the generalized n-stream version would look like!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it better to just do everything infix? That seems more consistent with most of the other methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good point. Probably only variadic combinators should have a prefix form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And yes about generalizing further to multiple streams! But that I think we can save for another PR! 😃
|
This is ready for review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work @adamgfraser!
|
|
||
| s <- ZManaged.fromEffect(loop(false, false, s, queue)) | ||
| } yield s | ||
| self.combine(that)((false, false, Option.empty[Loser])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is so nice!
|
@iravid Thanks for all your help! Excited to get this merged! |
|
@adamgfraser Happily and thank you for working on this - I was wondering for a long time how we'd abstract over all of these binary merging operators. |
|
@adamgfraser seems the test |
|
@ghostdogpr Let me take a look. |
|
Might also be related to the |
|
@ghostdogpr Have you rebased against master? I'm not able to reproduce locally. |
|
Yeah it happened on a PR that rebased against master. The failure only happened once and disappeared after re-running the build. |
Adds an
interleavemethod onZStreamfor deterministic merging of streams. Uses a stream of boolean values to determine whether to pull from the left or right stream each time so could be used for simply alternating between two streams, pulling from each stream with different frequency, or using effects to determine which stream to pull from next.@iravid Would you take a look when you get the chance?