Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@adamgfraser
Copy link
Contributor

Adds an interleave method on ZStream for 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?

Copy link
Member

@iravid iravid left a 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](
Copy link
Member

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?

Copy link
Contributor Author

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 {
Copy link
Member

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 result

Copy link
Contributor Author

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],
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

@jdegoes
Copy link
Member

jdegoes commented Aug 7, 2019

@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?

Perhaps a unification might look something like: combine((dest, left, right) => ...), i.e. you get a destination queue (or at least the offer part of it), and you can push things there from either left, or right, in whatever order you choose; all scoped by Managed to ensure things cleanup properly.

@iravid
Copy link
Member

iravid commented Aug 7, 2019

Right, I agree with that direction. What do you think @adamgfraser? Want to give this a shot?

@adamgfraser
Copy link
Contributor Author

@iravid Yes, I'm on it!

@adamgfraser
Copy link
Contributor Author

@iravid @jdegoes Take a look at this.

left <- s1.toQueue(lc)
right <- s2.toQueue(rc)
dest <- Queue.unbounded[Take[E, C]].toManaged(_.shutdown)
_ <- f0(left, right, dest).toManaged_
Copy link
Member

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?

Copy link
Member

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 f0 with ZStream.unfoldM and unTake

Does that make sense?

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we can.

Copy link
Member

@iravid iravid left a 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))))
Copy link
Member

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)
Copy link
Member

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.

Copy link
Contributor Author

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] = {
Copy link
Member

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],
Copy link
Member

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)(
Copy link
Member

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.

Copy link
Member

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!

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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! 😃

@adamgfraser
Copy link
Contributor Author

This is ready for review.

Copy link
Member

@iravid iravid left a 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])) {
Copy link
Member

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 iravid changed the title Add ZStream#interleave Add ZStream#interleave, ZStream#interleaveWith, ZStream#combine Aug 9, 2019
@iravid iravid merged commit 652c3d5 into zio:master Aug 9, 2019
@adamgfraser adamgfraser deleted the interleave branch August 9, 2019 19:54
@adamgfraser
Copy link
Contributor Author

@iravid Thanks for all your help! Excited to get this merged!

@iravid
Copy link
Member

iravid commented Aug 9, 2019

@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.

@ghostdogpr
Copy link
Member

@adamgfraser seems the test interleaveWith is flaky, I’ve seen it fail on an unrelated PR.

https://circleci.com/gh/zio/zio/16550

@adamgfraser
Copy link
Contributor Author

@ghostdogpr Let me take a look.

@iravid
Copy link
Member

iravid commented Aug 15, 2019

Might also be related to the process refactoring as I haven’t seen that fail before.

@adamgfraser
Copy link
Contributor Author

@ghostdogpr Have you rebased against master? I'm not able to reproduce locally.

@ghostdogpr
Copy link
Member

Yeah it happened on a PR that rebased against master. The failure only happened once and disappeared after re-running the build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants