Devreal

Scalaz Stream: Rebirth

Event: Scale by the Bay

Scale By The Bay 2018: John A. De Goes, Itamar Ravid, Scalaz Stream: Rebirth

Recording: Scale By The Bay 2018: John A. De Goes, Itamar Ravid, Scalaz Stream: Rebirth

you John D goes and I'm a big fan of functional programming I'm also the big surprise I'm also the author and architect of scholars Ezio which is a zero dependency type safe library for doing composable concurrency high performance composable concurrency in scala so if you haven't checked it out yet I hope this talk will entice you to check it out because it's compatible with legacy code like Scala's future for example or Twitter's future even or even cats effect so please do check it out so a long time ago years and years ago there was a library called Scala Zed stream and this library introduced the Scala community to purely functional stream programming and it totally changed the game and now fast-forward many years later and there's lots of these libraries out there and they're very high-performance they have lots of features but Scala Zed stream hasn't changed in like really a long time two years I think since the last commit and honestly it's been even longer than that since the libraries had any significant amount of change and that needs to change so today I want to talk to you about some of the work that's gone into updating Scala Zed stream for the modern era so this is an area in which we have very powerful effect data types like Zeos IO monad which allows you to do parallelism and concurrency has automatic interruption and resource safety and so forth so this is Scala Zed stream but updated for the modern era of super powerful effect types and it's not just my work by the way I want to recognize the contribution of Itamar over here who hit me up like three months ago on Twitter saying are you working on Scala said stream and I said yes can I lend a hand and I'm like what you want to spend your nights and weekends writing all this code much of it throw away because you have to do a lot of prototyping in these situations helping me produce the code for this presentation you said yeah sure hell yeah bring it on and so in Amara has been working tirelessly over his nights and weekends with me bringing scholars Ed's stream to you all today and he also by the way travelled all the way from Tel Aviv Israel right to give you this presentation today so please join me in a round of applause for all his hard work he's worked very hard in as have I I hope so everything you're going to see here is being seen for the first time in this room today with you we'll do a pull request a live pull request at the end of this presentation that contains all the source code including some tests and some documentation and some benchmarks and we're gonna reveal facts that no one knows about because Tamar and I have worked under the strictest of confidentiality nothing has leaked so now's your chance I hope you will leak everything you see here and if you do if use one of those hash tags I'll go ahead and retweet it I'll talk a bit about the motivation for why are we doing this today give you a sneak peek into the API so how does this thing actually look because that's what you probably all want to see is how do I use it in an application to solve the problems that I have well take a look at performance performance is actually not the number one reason to use Scalzi stream so there's actually some other I think more important reasons but in order to entice people to use scholars that stream we can't do too terribly when compared with the competition out there so we'll make sure that we we don't do too bad in comparison with acha streams and other solutions and then finally i'm going to give you a sneak peek at the design so what's under the hood of this how how does this think tick and I think that's going to be very interesting because we are doing some things in a streaming library that have never been done before so it's a different type of approach than you're probably familiar with so Y streams well streams are really good at composable leak free data processing so you can do all the same stuff you do with streams with an imperative give me an X batch of data and I'm going to transform it and do something else but then your solution becomes in parrot of knots to declare deaf so it's harder to reason about and also you have to manage resources yourself so you have to open stuff and close stuff and that introduces possibility of air and it's just sort of mindless boilerplate plumbing we'd much rather focus on the core of our problem than all this mechanical details of getting resource allocation right streams let us do that also streams are phenomenally good at incremental computation so with event sourcing architectures and and so forth fast data they're really really good at doing that and then finally streams allow you to work over infinite datasets without having to think about it because streams are designed to work in constant space even when they're working over massive datasets so you don't even have to worry about the size of your data set you just use the simple API and then the library handles the heavy lifting of making sure it gets done in constant space yzo stream it turns out that there are three buckets three main buckets in which streaming solutions fall and one of them is sort of file in socket processing you build microservices web services using solutions in this category another one is graph processing and stream processors so these are big topologies and events go from one to the other and you have very very sort of dynamic capabilities processing large amounts of data using those and then the final category is sort of reactive style programming which even gets a bit into the discrete sort of FRP observables and and so forth and these are three different categories and so one reason to do Scala's dead stream is so we can focus in on one of these categories and do a really good job just servicing the requirements in that category and what we've chosen to do is focus on file and socket processing because most business applications need Web Services they need to do HTTP and RPC and so forth and so that's the dominant category here most applications fall there and and if you're not in there that it's a relatively framed use case so we're focusing strictly on that and so we are throwing away features we're taking things that you can do using some Stream libraries and we're tossing them in the garbage and in particular one of the things that we're tossing away a lot of Stream representations have a spine and you can deconstruct that spine pull out the head and the tail and then you can also build up that stream again you can rebuild the spine and that allows you to do recursive processing transformation on streams through uncon gene and decon Zayn and we're kicking that out you actually cannot do that with this library and why well again the more stuff you throw away then the stronger guarantees you can provide on on the back end and you actually don't need to do spine deconstruction and reconstruction for the vast majority of cases out there and then finally we're more or less giving up the ability to create graphs that have cycles in them so you will use a library like this for directed acyclic graphs but not necessarily for those that have cycles in them and if you throw away all that stuff it turns out that you can do a really good job at the remaining use cases in particular you can make something that's aggressively lazy that does the minimum amount of computation using a pull based architecture you can make it so it's a hundred percent leak-free so you cannot leak resources under any circumstances and the user doesn't even have to think about that automatic resource handling and earliest termination so things will be closed at the earliest moment possible you can also create a uniform design that leverages features of the underlying effect types of today's modern effect types they have features like interruption and in zou get interruption for free and it's instantaneous you can interrupt any process that's running you interrupt it and it cleans up all its resources and you get resource safety and you can actually design your stream to use these same constructs some libraries out there have their own notions of cancellation and interruption and resource safety and so you have a mismatched model that makes it harder to reason about complicates the code bases and leads to leaks in extreme cases and and with zero stream we don't have any separate model for these things that's it's totally unified it's a uniform model of resource handling and interruption type inference with zero stream is extremely good thanks to use of lower kinda types and appropriate declaration side variants you should never need to specify any types which makes the library exceedingly pleasant to use as an end user it's economical so instead of opting for a library that gives you 20 or a hundred different abstractions we've just given you a small number of things that compose together in ways that can solve the problems you have so there's not a big API for you to memorize you just get your head around a couple of different things and now you can use those things to solve the problems you have and finally it has really amazing integration with Zeo and takes advantage of features that are available only in Zeo like automatic fine-grained interruption typed error handling and very strong guarantees on finalization so you have very strong guarantees on when your finalizar is Rhon what order they run in and whether or not they run in the presence of failures to other finalizes and z is the only effect library out there that can give you those types of guarantees there's very very good integration with Ziya stream and those features so as john mentioned we have designed this call that stream the new version to be extremely economical in the amount of concepts that the user has to learn so let's see what this actually looks like so this is what we came up with to be the minimal set of abstractions needed what do you think oh I'm sorry my stalker streams sorry about that so this is actually the iostream you just need to learn two concepts stream and sync let's see how they look like so a stream intuitively is a producer of a values more precisely it is a program that will effectively produce 0 or more values of type a and may at some point fail with an error of type II now strings compose extremely well for you FB aficionados in the audience there are many abstractions that apply to streams from functional programming and they are very very well composed now let's see how you can actually construct a stream so to construct an empty stream you would just use the empty constructor which as you can see from the type signature will never fail and will never emit any value because of the use of nothing in the type signature to construct a string that will emit in one value we can use point and you can see again that will it will never fail and will emit a value of type int if you have an iterable you can convert it to a stream and you could also convert an i/o value to a stream using the left i/o constructor and see how the typed error from the i/o value gets lifted up into the stream now you could also convert a queue in zio queue into a stream that will continuously DQ elements from the stream as you use it from the queue Soren has use it and you could also use unfold and an effect will unfold to produce a stream from a seed and a stepping function and lastly of course as John mentioned we can use a bracketing construct to create a stream that will automatically open and close a resource without leaking it so to compose streams we have many combinators for your pleasure you could also you could of course map streams you can convert the elements of a3 from one type to another and if you'd like to convert each element to a stream inside a stream you can use flat map to create a composite stream which is the concatenation of all the smaller streams of course you can drop elements from the stream using filter and you can also concatenate two streams using the plus plus Combinator now here's the transduce Combinator a powerful Combinator that will let you state fully process elements from the stream and Cree and emit bigger elements from those smaller elements and you could also use scan to state fully map elements of the stream by carrying along some sort of state with you as you go along and of course we have take take while drop and drop while which operates very similarly to the Scala collections library so if you have intuition from these Scala collections library these mostly apply to stream as well if you'd like to apply an effect to each element of the stream as you go we can use for each for that and you could also combine two strings together point wise to create a newer stream a larger stream which is composed of tuples of each of the two streams and you could also interleave two strings together using the merge operator and you could also combine two strings together with a slightly more custom logic than zip using the join width and lastly we have the peel operator which will let you process a prefix of a stream for example to detect the content type of a entity HTTP requests 3 and then process the rest of the stream with remainder using another stream constructed using the value that you produced from the prefix so in a stream we also have sink so if stream is a producer of a values sink Dulli is a consumer of a values which will consume values of type a and will eventually return a value of type B using those values now it may also output a remainder of type a not may or may not because not all sinks must consume all they're in it so let's see how we actually use sink so up till now we haven't even seen how we actually run a stream so sinks are how we do that we have the run method on stream which will just spill the values of the stream into the sink thereby producing an IO of the result of the sink so few small examples here you could still a stream into a draining sink which for produced value or producer units you could decode a stream using a sink that will produce an i/o of a request and you could also collect all the individual elements of the stream into a list using a sink that collects them now sinks also compose extremely well there is a rich number of abstractions that applies to sink and it is very well composable and it's a joy to use for these abstractions now to construct a sink you can use the various constructors in the sink panel object so this sink for example will consume any value and will produce the constant value provided to point and this thing is very similar to point but it will consume any value and will produce the value produced by the i/o value and will also fail with the same error type for that IO sink top lift will take a function and consume values of type of top of the type of the sink pass them through the function and return the result in the sinks result sink fail will immediately fail it will consume anything will produce nothing and will immediately immediately fail with the same with the error type a weight will just pass through any element consumed by the sink into the result and drain we've seen that before it will just consume the elements and return a unit we've seen collect as well which will collect the elements into a list and fall of course we can construct a sink from a full we provide an initial state and a stepping function and the sink we'll just apply the stepping functions each element consumed eventually producing in this result type the state provided the state that it has been accumulated now of course we can combine sinks and compose them so it's just a taste of the combinators provided for sink we can map sinks by applying a function through their result type and we can also contra map sinks thereby adapting the values consumed by the sink using the using a function so here I am adapting a sink that sums integers into a sink that sums salaries for persons for the for this case class by contracting the sink I could also transform the error type produced by the sink by applying a function to the arrow type and I could also flat map sinks so if I'd like to produce another sink from the result type of the original sink I can use flat map to create a composite thing that will first consume elements using the first sink and then switch over to the swings that is produced using flat map and using flat pepper you can also create a define the tilde Combinator which will first run the first sync consume the elements and then switch over to the other sync and eventually produce a couple of the two return values of the two sinks we also have the question mark Combinator which will couldn't convert any sink of type a to a sink of type option of a and it does that by returning none for the result type whenever whenever the sink fails the original sink so you can see that the arrow type for the bottom sing has been transformed to nothing so we also have sinks Combinator's that runs things concurrently so the brakes both Combinator will run two sinks in parallel and return the result type for the sink that finishes successfully first or else is similar but will always prefer the left hand side sink and finally we have sink repeat which will repeatedly run the underlying sink converting its result type to a list and will collect all the results in toilets so these are just a small subset of combinators we couldn't fit all the combinators on these slides for a sink and stream and I think you'll be very interested to see all the commentators that we came up with and how rich they are when we open the poor quest so performance how fast is Scala's headstream like I said before a performance is not the reason to use scholars at stream rather some of the other things that we've talked about like resource safety and so forth tight integration with video those are the reasons to use scholars at Street but that said we can't do too badly against the competition so we've done two benchmarks so far we'll do others over time the first one starts with a stream of integers 50 million and filters just four even integers converts them to lungs and then sums them all up so very simple simple enough that I can show you the implementations in a line of code we start off with akka stream so in the most straightforward way constructing a naka stream that reads all these different integers filters them converts them to lungs and that ends up folding them in Anoka as you can see does fairly respectably actually it takes about 10 seconds to grind through 50 million integers which is pretty darn fast what do you think about 50 million integers in 10 seconds and that's definitely not something to laugh at and then fs2 the logic remains mostly the same just adapting a few things to the fs2 api and amazingly so really amazingly because keep in mind here fs2 is purely functional library and akka stream is not AFS to performs ten times faster than akka stream on this benchmark which should get you to start paying attention to what the functional programming community is doing because we really are trying to make purely functional programming practical and results like this really show that you can do that and then the scholars said code is basically the same not many changes here only the difference is it runs 20 times faster than akka streams and then we thought well maybe this is just an aberration we should do more realistic benchmark amigas most things are gonna be doing more than summing numbers so we looked at the problem of coconut tokenizing a very large CSV file with potentially many columns the columns could be outrageously long so a very uneven distribution and it's going to be chunked it's going to come in as an array of characters and then the codes gonna have to piece these things back together and find the separators and produce a stream of well-formed CSV tokens accumulating leftovers like between chunks you could split a column between chunks so you have to reconstruct that in the process and you have to do all that in constant space-time so that's the process and the akka stream version is very straightforward you just use scan on there and you can end up building these things and you mid in the omitting them as you go and that actually performs reasonably well given the the size of the CSV file and then the implementation for FS 2 is basically the same code because we have all the same operators just a few FS to ism's it performs extremely well ten times faster than the akka stream version and then we move on to the Scala z1 which is slightly less code but same basic idea here and same basic structure and it actually performs three times as fast as the FS 2 version which in turn is 10 times faster than the akka stream version so we're up to 30 times faster than akka stream in this more realistic benchmark of what you can do here so let's take a look at how we pull this off what's going on here why why is this library so fast and why is it able to provide such strong guarantees around laziness and resource handling ok so how would you build your own streaming language well there are basically purely functionally there are basically two approaches you could take you could use the initial encoding whereby you define the stream as a some type and whenever you'd like to add an operation dream you define another term and that's some type so for example in meeting an element would be a term in that some type that will emit the element provided mapping would also be a term in that some type that wraps the underlying stream and keeps the function that is mapping around so as you go and you add the operators you end up with a couple of terms in your some type and that's pretty easy right think of an operation just add a some to the some type that's pretty straightforward eventually though you'd need to actually run the stream and then you'd use an interpreter function that pattern matches on the terms of the some type and performs the effects required within the target effect type so for example an i/o so that's how you would go about creating a stream using an initial encoding the other way you could go about that is by using a final encoding so a final encoding involves choosing a target representation which in our case would be a function taking a call back that will repeatedly call that call back for each element of the tree so emitting an element in the stream would be a stream that calls its call back function once with the provided element mapping would similarly be applying the function to the elements before calling the callback so you see that you need to reason about the final encoding itself the representation as you add the operations which means a bit more work and you need to reason about these pretty strictly because if you can't you can't add the operation now these two approaches have different trade-offs so initial encoding is pretty flexible there it is very easy to add new operations you just add the term and you add the relevant case and the interpreter while the final encoding is kind of long because you need to implement it right as you add the operation and if you can't implement it in terms of the target effect you can't have the operation in the initial encoding you can also perform some optimizations so for example as you pattern match on the stream if you see that the underlying stream in the map case is also a map you could fuse the two functions together before applying these than to the element you can't do that with final encoding you can't look ahead in the industry because all you have is a target representation of the final encoding and all you can do is execute it now initial encoding is unfortunately because it is so easy to add operations they tend to accrete many many terms in the in the some type of the stream which in turn leads to very large sprawling and often leaky interpreters being produced for interpreting the stream itself on the other hand the final encoding because it forces us to actually encode the operation in terms of the encoding this usually results in much more structure than principled operations for the stream and finally initial encoding czar often forced to replicate features from the target effect like cancellation interruption and resource safety and these replications these reimplementation x' must be very tightly matched or very carefully matched to the to the target effect which is usually a place where errors and leaks can occur and this cannot happen with the final encoding because the target effect type is already there in the final encoding you are already using these features from the target moment so these are the trade-offs so how do we do skál Zed Street well there's a very famous way to represent streams that was discovered in 2003 by a guy named Oleg I can't pronounce his last name otherwise I would try but he discovered that you can actually represent streams as left folds over the stream of elements very simple idea and extraordinarily powerful because it gives you laziness because you only need to fold for as long as the function that you're passing to the fold actually wants to continue reading so you have the ability to do short-circuiting which means you can read the minimum amount necessary from the stream you can also do processing in constant space very trivially you can also very easily implement this in terms of a final encoding so you don't have the drawbacks of the initial encoding and when you do streams by left folds you automatically have safe resource handling because the function that is executing the fold can control when it acquires the resource and when it releases the resource and it's known when to do that you acquire the resource and then you fold over the elements using the function that you're provided with and then you close the resource and it's really that simple and so it's very very simple to create leak free stream processors using literate ease unfortunately oh well here's an example of what an iterative looks like so an iterative could either want more data or be done with some value and this looks a lot like sink this is this is actually very similar to the model used by sink and then the enumerator which is our version of stream is the left fold you give it the arity which is basically the function you you pass to the fold and it's going to run that energy until the energy is done it's just gonna feed it lots of elements and then it's going to end up returning the result returned by the energy or maybe air along the way if something bad happened this is you can actually express this run function which I just showed you in terms of a more primitive fold function and in the fold function should look exactly like the folds in the Scala collection library except it's short circuiting so you can choose to be done with the fold before you've reached the end so this is a short-circuiting left fold and this is the essence of stream in scholars Ezio is this fold like function now you pass it a function and it's just going to consume stuff and eventually it's going to terminate with its gonna it's gonna keep on going until it reaches a point where it can produce a B value and then it's going to stop and then the fold function that's calling that function then and iterating over the stream of elements it can then return the B value from there you can define functions like map on this notion of a stream and it's very simple to do that you can define flat map and filter and all these other ones and the implementations for these are remarkably simple just a couple lines of code and you can define them filter this filter they're all amazingly simple how simple you can add these Combinator's unfortunately in Artie's suffer from three horrible drawbacks that have rendered them not suitable for production and the first one is you can't combine different sources you can't combine different streams into a single stream and that's pretty serious limitation because a lot of work we're gonna have to be opening this and then opening this and then combining those results in some fashion and just that linearity requirement of iterative it's just too much we can't build tons of applications with that we can build some but we can't build a lot also iterate sees are not very good at dealing with leftovers so sometimes we want to parse a bit of a stream like headers from a request or from our PC stuff encoding information and then we want to take a big long body that can't fit in memory because it could be gigabytes and we want to process that separately we want to Transco to turn it to Jason or records or whatever and inner tees aren't good at doing that they want you to compose all your inner at ease together and then run the stream with a single energy that's what iterate sees one compose all your inner tubes together and then run the stream with that single energy and oftentimes it's not convenient or easy or even possible to do that and finally these notions of it or tease that I just showed you they're not stack safe and it's it's hard to make it's possible to make them stack safe but nonetheless in this encoding is not stack safe and it's not obvious how you would make this encoding stack safe so is that really true though is it is it true that we can't combine them is it true that we can't deal with leftovers is it true that we can't make this stack safe well let's take a look at them all like produced a paper showing that it is actually possible to do parallel composition of schemes using iterative and the key thing here is to focus in on the type of this energy this is in Haskell code I apologize but the Scala one will be far more hideous I assure you focusing on the type of this energy and you can see something about it it is mimetic so these are min attic it Arty's these iterative ZAR actually a monad transformers and that means you can take any effect and you can plug them in there and when you have this capability when you turn your inner T into a monad transformer then suddenly you can come up with a notion of parallel reading from two different sources in parallel by using a inter at T monad applied to some effect type F inside the monad slot for the outer inner T and then you can define the lift functions so you can take something in the inner one and lift it to the outer one and then you can define combinators to read from the left side and the right side and if this sounds monstrous it's because it is and it's way worse than you think it is I implemented this in 2012 and it took gigabytes of heat space just turning on a relatively small file this is not something that JVM is good at you can do this in Haskell you cannot do it in Scala and it's monster it's no type in for instance you specify types everywhere this is not practical okay so let's take another look at stream this is closer to the representation used by scholars ed zio stream so in here notice that's the function that you passed a fold can return an i/o embedded in modern effect types like IO is actually a continuation monad and a continuation monad gives you the ability to suspend computations what that means is as the fold function is calling the step function the step function can suspend the function doing the fold which gives you the ability to do amazing things like transferring between streams and let me show you how simple this is here's a function that converts a stream into a queue and it's amazing that only takes like five or six lines of code to do this and it does this by suspending the stream that's doing the iteration over the function that you pass to the fold it suspends it until you pull something out of that queue and then at the end of the day this is wrapped in a managed resource so you have to use this resource when you're done using it what happens is the stream doing the iteration is interrupted so this takes advantage of two features of zo seamless pervasive interruption and guaranteed running of finalizar x' when anything is is terminated so these the combination of these two features in zo make it possible for us to implement this to queue function and once you have this two Q function implementing things like zip becomes trivial utterly trivial and is almost no work almost no code it's it's amazing how simple it is to do when you have an effect type there that supports continuations so we can combine them and in fact not only can we combine them but we can do leftovers so you can write this peel Combinator which lets you peel off a chunk of a stream and then do stuff with that like based on the that whatever you peeled off you can choose to do different things with the remainder of the stream and that's possible using the same mechanism and then finally you can make it stack safe by trampolining the function so in order to apply the function you actually have to you just stuff the function in i/o and suddenly everything becomes stack safe so we can do combining leftovers and stack safety which is amazing we can get all the benefits of it at ease but with modern effect capabilities like interruption and resource safety and and so forth we can also combine streams we can we can handle the leftover problem and we can make it all stack safe so where to from here well we've got to do some additional work in performance so we've got to do there's some types of fusion that we can perform which we are performing for some operations but not all we've got to work on the performance of Q because Q or Q like abstractions become extremely critical to combatant combining different streams so we've got to make sure the performance of those are high and then complex Combinator's can be simplified the logic of their the allocations and overhead of those things could be simplified so even though 0 stream is very very fast we can make it faster and also we can make it faster in the more complex cases like combining lots of different streams into one we need better tests and documentation so comprehensive test suites and great documentation across the board there's already some Scala doc but we can do better and then we need more combinators just to do the common stuff and just spend some time getting that set of combinators really right so it's the right set that you need to create all the the solutions for all the problems that you have so a lot of work has gone into this over the past couple months it's been nights and weekends work and forward so I'm extremely grateful to hit immerse contributions because I certainly wouldn't not have been able to do 0 stream to this level of quality without his help but I also want to thank Eric tor bori whose presentation on beautiful folds a couple years back really got me started thinking about can we do streams in a different way and then all I get his work on inner T's which he's he's done lots of papers since 2003 just showing just how powerful this concept of lazy left folds can be when implementing streaming and then finally I want to thank the fs2 team including Michael proquest and Fabio and Pavel who have worked very very hard building what is sort of best-in-class streaming solution and it's he and his team he's raised the bar what you have to meet in order to interest people which is actually rather good Itamar and I spent quite a lot of time trying to beat fs2 in the performance benchmarks because it is well optimized it's good good piece of software so I want to thank them for doing that excellent work and just raising the bar on what's possible to do in the realm of pure functional programming in Scala so that's it we're gonna do the live pull request here in a second so any questions in our remaining 30 seconds or so yes yeah so I think the major differences are summarized in this early slide that takes a look at yzo stream so I would say so the leak free guarantee it's been this is not a knock on FS - but the approach they're using is very hard to reason about and so when you implement complex Combinator's it's very easy to create leaks and also there's a mismatch between its model of interruption and cancellation and resource handling and then the underlying IO monads model of interruption resource handling and cancellation and it's super hard to get that right and there's so many problems over the years with leaks just search for a leak in the issue tracker and pull request tracker and you'll pull up dozens and dozens and dozens of issues and some of them finalizes not running you know all kinds of just really weird things that happen because it's a it's a more complex model it's capable of more but it's also much more complex it's harder to get that stuff right and we don't have a good track record of building an abstraction that is guaranteed to be leak free the uniformity is something that's very nice the type inference superior type inference all around is quite nice it has even fewer concepts than FS 2 FS 2 is very minimal it only has a few different things but Scott zero stream has even fewer just has two things sync and stream and that's it and you can solve all your problems with those at least if they're in our target market and then finally the zero integration is really really good and you don't get that out of FS - there's no way to make FS to know about the things that are in zero and justin Xia right so most likely will use Zeo scheduled to do that because Zeo schedule allows you to very flexibly create repeat and retry schedules so we'll integrate that into into Zeo stream at some point he is about ready to push the pull request now it's bushed so it's online you could see all the code run the benchmarks any other questions yeah in the previous houses for you oh right we ripped that that was years ago like eight years ago so we ripped that stuff out and replaced it by something that was much simpler because it just it could not perform we we just used an effectual stream so it was just linear so you couldn't combine them so you had those types of things and you and we operated on a chunk basis and we solved the problems but it was very painful and didn't offer the rich composition that something like zero stream offers all right thank you very much for attending [Applause]