Extending Functional Pipelines with Gatherers - Venkat Subramaniam
About this talk
This talk explores recent advancements in functional programming within Java, particularly focusing on the concept of gatherers. The speaker begins by reviewing the functional pipeline introduced in Java 8, which utilizes the Stream API for processing collections functionally. They illustrate how to customize terminal operations using collectors and delve into the need for custom intermediate operations. With the introduction of the gatherer in Java 24, developers can create their own custom intermediate processing steps without waiting for language updates. The session emphasizes the importance of maintaining code simplicity and the need for thoughtful implementations to ensure maintainability, especially when dealing with stateful operations.
Full transcript
We're going to talk about uh uh rather newer advances in the functional programming area of Java. And this is related to extending the functional pipeline with this concept of gatherers. Uh this is a bit of an advanced topic. So, we first need to lay some grounds as to what this is actually useful for. And then we will dive in and take a look at some examples of
how we can benefit, how we can use it. So, let's get started. Well, the very first thing is we need to really focus on functional pipeline. I'm sure a lot of us have used functional pipelines, we've written code with it. But it wouldn't hurt to just take a quick refresher around what that what are we talking about. So, functional pipeline in Java was introduced in Java 8.
This is a way for us to do functional programming using the stream API in Java. So, when you look at an example of this, you could say I have numbers is equal to list of maybe you can have a list of numbers 1 to 10. So, essentially in this case, you can say that these numbers 1 to 10, we want to be able to process these numbers
let's say in a functional pipeline. So, you could say for example numbers.stream and then you could say for instance in this case uh maybe a filter. And you can say given an element, you are interested in element mod 2 is equal to 0, which means that we are only asking for the even numbers in this collection. Then you could perform a map operation where you could take
an element and you can double the element in this particular case. Then maybe you can use a for each and you can then say system.out and you can print it out and you're literally printing all the double of the even numbers in this collection. So if you look at this particular particular code, what we are achieving in here is the ability for us to iterate through these
values and to be able to print them. That's basically what we are trying to do here. I'm going to just restart this real quick and then and then we'll move forward. So let's go ahead and restart this. It's not picking up my changes just yet, but it will in a minute, I'm assuming. So essentially the idea behind this is we are using the stream API to iterate
and and to be able to execute it right in here. So So the question is what do we know from this functional pipeline that we saw so far? So when you look at the output, you can see it is displaying all the even double of all the even numbers in this collection. But having said that, we know that this is the functional pipeline where the data flows
from one function to the next function and so on. A few basics we need to really keep in mind. The first thing is that a stream is is an API that has lazy evaluation. So essentially lazy evaluation simply means it will not execute certain functions unless it really has to execute. So that provides efficiency. However, the functional API contains two types of functions. What are called in
this particular case, what are called intermediate operations and and then of course, you have what are called the terminal operations. So essentially a functional pipeline is made up of these, you know, zero or more intermediate operations, and typically one terminal operation. Now, what is really interesting in this particular case is that the terminal operations generally we were able to customize them. How do you customize a terminal
operation? Well, if you want to customize a terminal operation, rather than calling the for each, you can call the collect function. So, if you call the collect, the collect function would receive what is called a collector, and you can implement your own collectors. And when you implement your own collectors, you can do your own custom terminal operation. So, this gave us an ability to implement a functional
pipeline, but to customize those terminal steps. This has been possible since Java 8 time frame. So, we could do this all along. So, that's great. So, we talked about the intermediate operation, and the collectors allow us to customize the terminal step. But, the question is, what if we don't want to customize the terminal step, but what if we want to customize the intermediate step that you see
in here? So, in other words, we want not a filter or a map or one of the other functions provided in the functional pipeline, but we want our own custom function to be implemented right in the middle. Now, this depends on the use case that you are actually programming for. It depends on what you're trying to implement for your business. So, it's not something you would just
randomly do. This is something you would do because your business analysts or business folks are coming and asking you for something, and you're like, "Gosh, how do I implement this?" The question is, why do you want a custom intermediate operation? The reason is intermediate operations are lazy. So, they don't run unless they really have to execute. So, that means it's more efficient to have these intermediate steps
in your application. So, that is the problem overall we are trying to solve here. So, I'll give you an example of this uh to think about why you would probably need something like this. Imagine for a minute that you are programming in Java 8 for a minute. And in Java 8, you had these uh functions like filter and map and all of that. Those functions became available
in Java 8. But, what if you want to process this pipeline, but you want to exit right in the middle of the pipeline? Maybe you are saying, excuse me, keep processing the values, but the minute the value that you get is more than, let's say, six, you want to break out of the iteration. Well, in Java 8, you couldn't do this. Well, in Java 9, they added
a function for us. And that function they added in Java 9 is a function called take while. And that take while says, given an element, when the element is less than or equal to six, go ahead and continue, but if it's more than six, break out. So, notice it stopped with the value of 12, which is twice the value of six. If I comment that line out,
it goes up to 20. So, this take while, as it turns out, if you notice, take while, the take while function was introduced as part of Java 9, as you can see right there. It says nine. So, this was introduced only as part of Java 9, but what if you were in Java 8? Well, sadly, you couldn't do this in Java 8. So, now you're like, "Huh,
I want my own custom behavior. I want to be able to break out of the loop in the middle. How do I do it?" Well, it turns out there are only a few options you had available. One option was you could go to the JDK team and say, "Excuse me, it would be so nice to have this. Can you please do it?" Well, maybe they will say
yes because it's a very general-purpose thing, but then when would you have it? It may be a year and a half before you can have it, not yesterday. Why? Because they may have to go through a pre-release, and eventually it gets into the product. That's not a time you can afford to wait. Or if it's not general-purpose enough, you cannot ask them, and they're not going to
accept it. And then you're back to square one. How can you implement it? So, the short answer was, "Good luck. You cannot have your own custom things." So, as a result, you had to use what was given and find workarounds. Now, this was the state of the affair until Java 24. So, in Java 24, which is which was roughly about a year ago, they decided to introduce
a way for us to be able to add our own custom intermediate operations. This is what is called a gatherer. So, a gatherer is a way for us to implement custom intermediate steps within a functional pipeline. So, in other words, just like take while, which was added much later, if you want to do your own thing, now you can do it by yourself. You don't have to
wait for others. Of course, one of the things to be careful about is this I would consider to be an advanced uh topic. This is not something you want to just do it without thinking about the consequences because remember, the problem with writing code is that somebody has to maintain it. So, the minute you write the code, you might may may may be the person or you
leave the company, the company and the team still is holding on to the code and they have to maintain it. So, if it is not needed, don't do it. And And so, if you think you need to implement it, what do you do? I've got some recommendations. You're sitting and writing some code and you're sitting there and thinking, "Gosh, I need a custom step. What do I
do?" I've got three recommendations for you. The first is, don't do take the time to see if there's a way to implement that custom step by using a combination of pre-existing functions like filter, map, and so on. Now, I really hate doing this to people, right? Because a lot of thought of times, people are really excited about what they do. But, I've received some emails from people
who are so eager, they say, "Oh, Venkat, I know you like gatherers. I wanted to implement this. Ta-da! Look at my gatherer." And they send me the code. And And I feel really bad when I see this because I would read the email and I would reply to them saying, "Good job. You really got excited about it. But, I'm really sorry to say, here's an example of
how you can do what you did by using filter and a map a few times. You didn't have to implement the gatherer at all." This is something I would encourage really well. Don't get on to doing it until you really find out you cannot solve it with what you do. That's my first advice. The second advice is, you feel like you really need to implement a gatherer,
I would say do call a good friend. And you tell your friend, "I have this urge to implement gatherer. Can you please help me?" And your friend says, "Let's meet for dinner tonight. Let's talk through this." And take up their offer. Meet them for dinner. Help them convince you not to do this. You'll be ever so grateful you didn't start writing it. You say, "No, Venkat, there
was no way to do this, and my friend couldn't convince me. What do I do?" Get very strong several cups of coffee and sit down because you need all the caffeine you need to get through this. So, that's basically what we are going to be looking at now is how can we implement our own custom gatherers. So, let's talk about an approach we will take towards this.
What can we do? We're going to ease into this, and one of the things you really want to do is to use a built-in gatherer than implementing your own gatherer where possible. So, why should we use a built-in gatherer? Well, they took the time to implement it. Always it's better to use something others are providing than to just simply reinvent the wheel, right? So, use built-in gatherers
if it's available. And also, every time Java is really a new release of Java comes out, just take a peek to see if there are any new gatherers they have added. So, if they've added in something new, that's good. Next time you have a need, you know what to go look for rather than trying to implement your own. This is also a nice way to understand and
think about gatherers, so let's do that right now. So, let's take a problem that we are very familiar with, and then we will come back and talk about maybe a custom operation that we couldn't do how gatherers help us to do it. So, I'm going to say result is equal to numbers.start, and in this case, I'm going to say a stream, and we will simply use in
this case a reduce and a zero comma integer sum, and I'm totaling the values, and once I do, I want to output the result of this operation. So, as you know, what does a reduce function do? A reduce function, sometimes called as fold left, takes the values in a collection and performs the operation, reducing this to potentially a single value. In this case, we are performing the
sum, which means it's going to take the zero and add it to the one, that's a one. Take a one to add it to two, that is a three. Take the three and add it to the three, that's a six. Take the six and add it to the four, that's a 10. Take the 10 and add it to the five, and that becomes a 15. So, essentially,
it's going to be totaling those values like so. So, if you look at the output, that's 15, because it just added each of the values starting from zero. It's an accumulation of the values. So, essentially, 0 + 1, result + 2, result + 3, result + 4, and so on. Well, this is available in Java from day one of functional programming, which is starting from Java 8.
However, there is one problem, though, that Java doesn't provide that other languages often do and other libraries often do. And that is, when you have this functional pipeline, as the stream is executing this this particular flow, the data is flowing through the pipeline. The data is flowing to the map, or whatever operations you have, and comes to the terminal operation. In this example, we didn't have a
filter or map, but you could have had anything in between. But, what does this do? It is taking all this data as it flows to the pipeline. In the very end, after it performs all the operation, it gives you the result, which is 15. But, think of a different scenario. Imagine this data is not some static data like 1 2 3, but imagine this data is about
a sale. So, the sale information is being sent to you periodically, and you need to provide an accumulation of the total sale you have you have received so far. But, if you only do this in the reduce, everything is going to be quiet for a long time, and then suddenly you're going to pop up a result and say, "Here you go. That's how much sale we have."
But, your business comes to you and says, "No, no, no, no. We don't want this to be silent for so long. We want you to keep telling us what the sale is, and then in the end give us the final result as well." So, this is basically you want this to be chatty. So, in other words, as the data is going through the reduce, and as you
are computing, you want to emit the partial results along the way. So, in a sense, if you notice in this example, you would gave a result of 15 in the end. So, the emit happens at the very end of this, and you got a 15. But, what is not just the 15. You want to be able to emit 0 + 1 is 1. You want to emit
1 + 2 is 3. And you want to emit 3 + 3 is 6. And then you want to emit 6 + 4 is 10. And then you want to emit 10 + 5 is 15. So, you want to emit each of these intermediate data as the computation is forward progressing. Well, Java doesn't do this. You may say, "But, I use this in Scala, or I use
this in a reactive library. Why can I not do when they are providing solutions to do it? And if my business wants to do, how do I actually accomplish that? Well, this is where an example of a custom intermediate step. So, how do I implement this custom intermediate step? We can do this all by ourselves thanks to gatherers. However, the people who worked on gatherers, Victor Klang
and the team, decided, "Hey, why don't you we use this as an example to illustrate how gatherers actually work." So, this is one of the built-in gatherers they provided. The operation I described, even I is often known as the scan operation. So, it's no surprise they called it a scan built-in gatherer. So, what I can do here is I can say numbers.stream and then I'm going to
use the gather, as you can see right there, and then a forEach. And in this case, I'm going to simply and print line and print that value in the end. But, what in the world is a gather? Well, you know about filter, you know about map and limit and so on. Gather is the intermediate function you will call to implement a custom intermediate step or a gatherer
step. So, in this case, I'm going to say gather and this is going to be the scan method. So, the scan method is going to take as an argument a value of lambda, rather, which says, "I'm going to return a zero as a initial value, an integer sum." Remember, that's what we did here in the reduce, and we are calling the integer sum here. So, what is
it going to do when we execute this code? So, notice that's a 15 from the reduce, but what did the scan do? It emitted a one, a three, a six, a 10, and a 15, which are all the intermediate operation of the sun while it is continuing through this particular processing. So, a scan is a very chatty reduce. So, a reduce is quiet, right? A reduce just
waits for finishes all its work and gives you the final result. Scan is chatty. It's going to continuously as it's working keep emitting the data, all the intermediate values it's going to be providing along the way. So, this is an example of how we use a pre-built function which is called a scan. You say, "All right. So, we used a pre-built function, but what if we want
to implement our own gatherer? How do we approach doing this?" So, this is where there is a gatherer interface. So, as you can imagine, the gather function takes as an argument a gatherer interface. So, if you look at the gatherer, the gatherer is an interface that's going to be used by the gather method to provide the way to do the custom intermediate step. Now, how does this
actually work? So, you can implement your own gatherer and you can get the code implemented. But, the first step is we need to understand how the intermediate step is going to work. So, to understand this, let's step back for a minute and understand about the So, when you have a pipeline, what do the pipelines have? You have a stream and then you have a stage, let's say,
one, a stage two, and maybe a stage uh three, and so on, right? So, you have these s- stages, one stage after the other, and you're going to be having these things, and finally and terminal uh step uh you know, step that you're going to have in the very end of this. So, given this, how does this actually work? Let's consider a stage in the pipeline. So,
you have the data coming in, you have a stage n, and you have a data going out of that stage. So, essentially, when you look at a stage, what do you have in a stage? Elements are coming in, and the elements are going out. So, what you deal with the data is an element. But, think about this for a minute. Remember, streams are lazy. So, because streams
are lazy, what does that mean? A stage may do the work, or the stage may not even be executed. If there is enough result already, there is no reason for it to run. So, if you have a pipeline, you have a find first in the bottom, then the minute it finds the first value, then remaining steps are not going to run. But, how do they know not
to run? So, we need to understand a fundamental. Let's assume I'm a stage, right? If I'm a stage, there is upstream, there is a stage that is sending data to me. There is downstream, there is a stage to which I'm sending my data. So, I am getting the data from a stre- stage, do my work, and sending that data to the stage down. So, my job, whatever
my step is, I'm going to be given an element. I perform my action, and I push the data to the next stage. That is my job. Once I push the data, the stage next to me says, "Thank you for the data. I am ready to get the next the from you." Or, "Thank you for the data. Could you please stop? I don't need any more data from
you. And that's how a stage knows, oh, so I don't need to work anymore. I can close the shop and go home because I'm lazy evaluation. You tell me don't do any more work. I'm not going to do any more work. So you need to understand the steps a stage is going to take. So a stage says, I got the data. I do my work. I push
the data to the stage. The stage tells me whether to go continue or not. And if it says continue, I'm going to wait for the next input. If it says don't continue, I'm going to tell upstream, hey, I got word. I don't need to anymore. So I'm going to tell you don't send me any more data. So a stage will communicate to the upstream that it doesn't
need any more data. So let's summarize this. A stage pushes data on the condition upstream. So data goes downstream. Here is the data. Push, push, push. Oh, condition upstream. I don't want any more data and I want to convey that upstream. So my downstream says, don't send me any more. I tell upstream, don't send me any more. So the data goes downstream, condition goes upstream, right? So
that is something for us to keep in mind. Now, one of the challenges you're going to deal with is this is easily said than done and and the complexity in here could become a problem if you will. So let's talk about what could be become the problem in this particular case, if you will. So to understand this, let's try to write a little example of uh uh,
we could implement a custom uh, intermediate step. Now, you don't want to write a custom step for what's already provided in Java, right? That's kind of pointless. But, I'm going to do it now just to give us an example, as an illustration. So, we all know how a filter works. So, what is the job of a filter? A filter says, "I have a condition with me. A
element is given, and I evaluate the condition. If it is true, I pass the data downstream. If it is not true, I don't pass the data, right?" That's what a filter does. A filter either pushes the data or it doesn't. We don't, of course, need to implement filter because it's already given. But, I'm going to implement a filter our own so we can see how to implement
it. So, let's think about how we can do this in here. So, I'm going to .filter, given a element, element mod 2 is equal to 0. .for each, system.out.println. So, we are using the given function right now. So, if I run this, you can see it's all the even numbers that got printed, 2 4 6 8 and 10. Okay, so far so good. But, notice I don't
want to use the filter right now. I want to use a custom filter. So, gather, and I say custom filter. And what am I going to give to the custom filter? Given a So, our goal is to implement a custom filter which does exactly what the filter does, just so we can understand how this works as an exercise, as an example. So, how do I implement a
custom filter? Well, as it turns out, what does the gatherer expect? It expects an implementation of the interface, which is the gatherer interface. So, you can imagine custom filter is going to return to you a gatherer, right? So, what is the return type of the gatherer? I'm sorry, what is the return type of the custom filter? The return type of custom filter is a gatherer. Can anyone
guess what's the type of the parameter for the custom filter? What's it going to receive? Anyone? You should be able to say it's spot on because it's right there on line 11. Predicate, right? Absolutely. It's taking a predicate. So, the input parameter type is predicate for a custom filter in this case. The return type is a gatherer, right? So, let's implement that function. So, let's go back
here and say, here is my gatherer and what does my gatherer do? My gatherer is of type integer, as you can see. And then, this What is What is this? What is it going to return? It's going to return an integer as well. So, you say, wait a second. I can understand what this is. That's an input type. Got it. I understand what this is. That's an
output type. What in the world is that question mark? The question mark stands for what is called the state. Let's just postpone talking about state for a few minutes. We'll come back to that after this. So, just keep that in mind. It's for a state. And then, of course, what does a custom filter take as an argument? A predicate of integer and we'll say this is my
predicate I'm going to be able to take as an argument, right? So, that's what we're going to pass to it. So, then the question is, what am I going to do within the custom filter? So, we're going to be able to implement our own gatherer. Now, remember gather is an interface. How do you implement an interface? You know, there are two ways to implement an interface, right?
You can say class my class implements that interface, which means you're going to write a separate class in a separate file. There's another way to implement interfaces. What is that? Anonymous inner classes, right? Yeah. So, you could say new gatherer and you can implement that right there. Now, something tells you you don't want to do either of those approaches, right? Why? Because if you write your own
class, that's a bulk of code you have to write. If you use a new and do a anonymous inner class, that's still a bit of a code which is verbose. So, what if we can find a way to reduce this verbosity, make the code really compressed, and make our life a little easier? Well, thankfully they decided to provide some convenience functions. So, what is a convenience function
to implement a gatherer? So, you can say return in this case, return gatherer of sequential. We'll understand what this word sequential means in just a few minutes. So, I'm saying gatherer of sequential and then what is this going to take as an argument in this particular case? So, we're going to say underscore. What in the world is an underscore? That stands for the state and I'm I'm
really not using state right now, comma, and we're going to say element, and then we have a downstream that it's going to take as well. So, as you can see in here, it is taking an a no state, it's got a element, and then it's got a downstream after that as well. So, it's going to take all these three things as an argument. And then it says,
"Now that you've given me those three data, what am I going to do? Apply predicate." And I'm going to call the apply predicate and send to it the element that we have. Well, first of all, the predicate, the element, and the downstream. So, I'm sending all those three in here to this function called apply predicate. So, if you look at this code we have, all we have
is off sequential, which takes these three arguments for the lambda, and and passes that on to apply predicate. Apply predicate says, "I'm going to return a boolean for you, and the boolean it's going to return is going to take a predicate, an element, and a downstream." Now, let's try to figure out what the apply should do. So, help me out here. Can you? I am the filter
operation, right? An element is given to me. I have a predicate with me, and I have a downstream right here. Right? So, here's the element, here's the predicate. What's the first thing you're That's right. Evaluate the predicate with the element. Good job. The predicate is going to turn true or false. Assume the predicate returned a true. What should I do now? So, here's the element. I gave
it to the predicate. Predicate said true, what should I do? That's right. She's right. Push the element downstream. With me? So, let's let's do that. So, we are saying if uh test element is true, then downstream, oops, then downstream {dot} push what is the downstream going to do when you give it the element? Imagine that. The downstream says, "Hey, upstream, thank you. You gave me an element.
That's nice of Is it going to return anything back? What do you think? Does the downstream return anything? The condition, right? Remember, data flows down, condition flows up. So, what is going to return back to the call to the upstream? A true if I want more data. And a false if you don't want any data. Make sense? What should I do? I I'm a filter. I push
the data downstream, and the downstream gives me a response. It could be true, it could be false. What's my job? What am I going to do with that information? When I get get it from the downstream, what should I do with it? If the downstream says, "Send me more data." If the downstream says, "Don't send me data." What am I going to do Right? That's why I'm
showing the direction of flow, right? It's a flow. So, what's my job? To send it up. Because if this says don't send me more, I should tell them not to send me anymore. If this says send me more, I tell them send me more. I'm a I'm I'm the middle, right? So, I just propagate it. So, I need to say return to the caller the state. Send
it up or don't send me Well, here's a question for you. Return true or return false? Question. I'm a predicate. I'm a filter. I got the element, evaluated the predicate, and the predicate was false. If it's false, what do I not do? I don't push it down. I just throw it away, right? So, I threw it away. Should I return or should I return What am I
going to return? True or false? False? Raise your hand if you said Raise your hand if you said true. Okay. It's smart people in the room. It's true. You're right. Why? Because a never has the authority to stop the stream. That's why it's true. You're absolutely correct. We need to return a true. So, we say return true, right? From this. So, let's see how this works. We
run the and notice 2 4 6 8 10, which is exactly all the even numbers from this collection that is coming through 2 6 8 10 and the odd numbers were dropped. If by mistake had you said false and this is a room full of smart people. You didn't say it. But if you said false, notice nothing went because it said stop, don't send me any. That
would have been a wrong answer. But because you returned a true, it did the right job. So, that is basically an example of implementing a custom you just saw the easy part. This is like watching a murder movie. You are starting and you saw some crime scene and you're like, "Ooh." And you realize you haven't seen anything just yet because if you felt the heat, it's going
to begin to start start sweating. the people who organized this conference were very smart. I usually do this over 2 hours. They only gave me 60 minutes. That's because they didn't want me to go any beyond I know the range when we have to call for medical help. So, we'll knock on this and get through a little bit more detail and then you can go further on
it by yourself or look at other videos that I posted on on YouTube. So, what are we going to do now in this? Find the big end a little bit. This comes to what's called the flavors What are the flavors of gatherers? For this, we need to understand a bit because the names are a bit confusing. Before we talk about the names, let's go back here and
notice we have a gatherer gatherer of sequential. See that? So, if you see the gatherer of sequential, you may say, "Wow, then I'm going to have a gather of parallel, right?" But, there is no such thing. There's no gather of parallel. Why? Why is there a sequential? Why is there's no parallel? Well, there's a very good reason. We're going to understand that in the next few minutes.
So, what are the flavors of gatherers? Let's talk about it. So, what we saw just now is the following. And here you can see this, right? So, we have a stage, and what did the stage do? It got an element from the previous stage. It does its work. It pushes the data downstream if it wants to push. The downstream returns a true or false. It returns that
to the previous stage. So, that's basically what we saw in the previous example, right? Good. Now, let's talk about the flavors of There are four things you need to think about. But, before we go further, let's understand about the functional You can say, you know, some data.stream, right? .filter, and maybe we can do a do a map, and you can talk about this pipeline that you So,
this is a stage, right? This is a stage that you have. Similarly, you have another stage sitting here. That's a stage, also. But, on the other hand, if you may have data.parallel uh parallel stream, and you may have a stage, and you may have a .map, which is which is also going to be a stage as well, potentially. So, in this example, if you look at filter,
and you look at map. What does filter and map do? Filter and map are going to execute their stage sequentially, right? Why is it sequential? Because you're working with a stream and a stream is sequential. So my stage says I get one data, I return send it off, get the next one, send it off, get the next one, send it off. That's great. I am sequential, right?
Now imagine I'm in a parallel stream. When I'm in a parallel stream, I am the middle part and data is coming towards me. My question is can I process this or only or can I process this in Let's think about this for a minute. If I'm a filter if I'm a my stage is a filter can I handle parallel execution or should I only do it sequentially?
If my job is a filter parallel, what do you think? Raise your hand if it's sequential. Parallel. I like your hands, two hands, I love it. This is sequential hand. This is parallel, I love it. Thank you. What do you think? One hand sequential, two hands parallel, what do you think? Yeah, parallel, right? Yeah, put the hand down really quickly. Otherwise people walking by will think you're
in an awkward situation, we don't want that. Okay. So it's parallel, right? Why? Because a filter says I'm just going to take it, evaluate, send it. I can do for one element or I can do it for 1,000 element, doesn't matter, right? So it can run parallel. let's say my stage is a map, not a Once again, map sequential Parallel, right? What about others? Come on. Say
something. Even better to say wrong things than be quiet. What do you think? Sequential or parallel? Thank you, parallel. Say raise your hand. I don't want to lose your voice. There you go. Awesome. So, filter can be parallel, map can be parallel, right? Fantastic. My stage is sorted. It's going to perform sorting of the Yeah, sequential, right? Why? Because it's got to sort the data. To sort
the data, what does it need? All the data. So, I'm in a parallel stream, right? Data is flowing towards me. What does the filter do? Filter is like keep pushing. I don't have to do the sequentially. Map says, keep pushing. Sort it says, hang on. You all line up now. And when you have all the data give it to me because I am sequential. Now you understand
why the function name is or off. I am a stage. I can execute in parallel. I'm off. I am a stage. I can only execute So, it's off sequential. So, essentially, that's why there's no off parallel because it's naturally parallel if you don't say it's sequential. In a sequential stream it's going to run only sequential. In a parallel stream it may run parallel or it may force
sequential. It is parallel stream, all right, but my stage becomes a bottleneck. That is the whole idea. That is why you have off or off sequential or off. Off sequential. In this example, I shouldn't have used off sequential. I should have used off. Why? Because you just told me a filter can run parallel, right? So, why do I want to limit it? And that's perfectly fine as
well. If it's in a parallel stream, it'll run Okay, we understood that. This is the are you sequential only or are you can run parallel? Let's shift our focus to something else. You got a data? I send it. Got a data? I send it. I got a data? I pass it out. Is a filter stateful or stateless? What do you think? Stateless. That's right. It doesn't have
any state. All the filter does is given a data, keep pushing it. You go to filter and say, "How's it going?" It's like, "What?" What do you mean how's it going? My job is to keep pushing data or throw it away. There's no going here. Filter is stateless. Map. Stateless up, stateful down. What is yours? Stateless? Stateless? Stateful? Stateless, right? It is stateless. Map doesn't care. Limit.
Limit three. I want to only send three values and no It's stateful. It is stateful. Why? It's got a number how many elements it passed through. take these two and slap them together. That's where the pain begins. So, now you know the four flavors of It could be sequential only or can run parallel. It can be stateless or it can be stateful. Now, one more question. Which
do you think requires more work to implement? Stateless is more work? Stateful is more more work. What do you Stateful is more work. Correct? Absolutely. If you're stateful and you're doing things sequential or you're doing things parallel which is going to be more work? Stateful sequential or stateful You're going to have state but is it going to be easier to do state with sequential or state with
parallel? Which is easier? Sequential hands down, right? Is is easier. So, I want you to think about this a little bit. You are here. Right there. That is walk in the park. You're walking in the park. You're happy. You're smiling, right? Because this is easy to implement. Why? It's parallel. It's stateless, no worries. Awesome, right? This is not a walk in the park. You have to implement
a sequential aspect. But the good news is it's still you just pop in a Tylenol. You said, "You know what? Just just a little pain. Dose of Tylenol. Okay, I feel okay. I'm fine." you are here. You are taking prescription medicine now, no longer Tylenol. The doctor gave you very strong medicine and a list of advice what to do. And you're not having fun at this point,
right? And you're on this prescription medicine, you're setting the clock and taking the medicine at that time. This is you are in the through your vein vein. And it's ready your it's reducing the pain, but you still feel it. That is basically what it is here. So, if you draw a C around it, that C, you're on the tip of the C on the top, that's the
comfort zone. And as you go from here down here, you just increase your level of complexity all the way. So, be very careful as you walk through here because it can get extremely hard to And the more you go towards the bottom right quadrant, the harder it is going to become in this particular case. So, you have to be very careful doing this. That's a lot of
effort. I want to decide between stateful and What do you think? Who is going to help us to decide that? Will the business tell us not in those words, obviously, right? They're not going to come to you and say, "This is stateful." But, do you make the decision of stateless or stateful by yourself or based on the business requirements? Who's going to convince you about that? Do
you come come to work and say, "You know what? Today is Friday. I'm going to take it easy. I'll write stateless." Or do you say, "Don't it. This is based on what the use case is." What do you think? What's your gut feeling? Business, right? If the business requires something and you have to maintain state, you got to do it. So, this vertical axis, stateless stateful, decided
by the business. Sequential versus parallel. Does the business force you that? Or do you get a chance to wait and see how the performance is? And based on that, you can decide it, You have a bit more leeway in there, especially in the bottom, when it's Writing it sequential is easier than So, why would you bother writing it parallel until you finish writing it sequential? And if
the performance is really adequate, don't worry about it. The worst thing you'll do is to implement it sequential and the performance is adequate and you go to your business and say, "You know what I could do?" And you ruined your life for a long time. Because a business doesn't know. they're like, "Oh my gosh, if we can make it faster, you should." Because anytime you give them
an opportunity for what is perceived as better, even though it's not, they're going to ask for it. So, don't go that route unless you have to. to drill a little bit into it, why care about the sequential versus parallel? What are the implications for it? So, I was thinking about what is an analogy I can use to describe it. And this is an analogy everybody in the
world understands, so it's easy to relate to. So, I want to you to think about this as the following. Only look at this part right here, the leftmost. This is like a freeway where there are four lanes for traffic. And what's happening in these four lanes? Cars are just zipping through, and there's nothing to stop them. By the way, obviously, I'm not talking about Bangalore roads, right?
So, I know this is a little hard to imagine. But but this is a lesson I've learned over time. This is actually true about Bangalore. But I figured this out. I've been coming to Bangalore for about 18 years now. But what I figured out the first few years, I made the rookie mistake. But these years, I figured out the best way to beat the traffic is to
leave town before the locals wake up. That's my strategy. I leave at 3:00. Be the only dogs on the road, no humans. You never have a traffic problem. So, when people say, "Ooh, traffic problem is bad in uh Bangalore." I'm like, "It's never a problem for me. I come after they sleep, and I leave before they wake up, right?" So, this is basically free flow of the
traffic, right? So, that is basically a parallel execution. That's what you see on the left. Look at the middle one. The middle one is a construction zone as we all have seen it. So, what happens? Cars are zipping through right there. And as cars are zipping through, what's going to happen? They say, "Slow down. There's construction." And they bring the four lanes to a single lane. We
all know how it feels, And and one car at a time goes the single lane. But the minute you clear the construction zone, you got four lanes again. So, you're parallel before, you're parallel after in a parallel stream. But that part is a bottleneck. And that is sequential. Just like how cars will negotiate and besides who's going to go before who, and now we are all lined
up and we go through that one lane until we get to the other end and we can just go in parallel after that. That is your stateful or stateless but sequential stage. That's like you are sorted. Look at the last one. That's the most complex. This is got a bottleneck, but you still want to execute that in This last one was the bottom quadrant you saw in
the picture. Most difficult You're going to take this parallel, but you're going to chunk them into groups and do them in parallel and then keep doing them in parallel until you get one two pair of values which you can put together and you can go forward. So, the bottleneck is resolved in parallel, but by merging these partial results So, these are some of the options for implementing
gatherers. As you can see, the complexity keeps increasing along the way. And as I mentioned, this is not a trivial concept. This is not a 101. You don't start doing this If you If somebody doesn't know streams, if they have not used filter and map, don't throw them into this. That is torture. They will They will absolutely hate everything else in life after that. But once they
are comfortable with using streams, understand lazy evaluation, understand intermediate and terminal operations, then they can eventually get into this once they get to a very very high comfort level using streams. So, this is not something you would just trivially get in and do in your application. So, obviously, I'm not going to cover all these other things as I mentioned in the beginning, that usually is a 2-hour
topic for me. But I hope that this has given you enough perspective on what really gatherers can do for you. So, my recommendation is to to go back to what we talked about. First, clearly understand what a stream can do for you. Clearly understand the difference between intermediate and terminal operations. Then, ask the question, can I implement a custom intermediate step using existing functions in the stream
API? That's the first thing. Second, if I cannot do that, can I use built-in uh gatherers to implement it? Third, if I cannot do it, can I talk myself out of it? Because this is going to be complex code that's going to take a lot more time to maintain, but is it worth really implementing it? Well, this is one of the things to keep in mind. I
had somebody absolutely sensible come to me and said, "Hey Venkat, I can take this time and effort to implement it. Great. But if I don't, I may have to do this in the old way of things that's available. It may take me a few extra steps, but if the data is really small, I'm not going to see much performance differences. Did I gain anything from it? I
immediately gave him a hug. Because that's the sense we need to have, right? That's the wisdom to say, yes, I can put that effort, but the time and effort I put in is not going to get me the results I want. My time and effort is valuable elsewhere. And and so ask the question genuinely. And if you then say, yeah, in this particular situation, we really need
to do it. Then take the time to implement it. And once you decide to implement it, what's the next step? Then ask the question, can I implement it stateless parallel or stateless sequential? Do I really have to go to stateful? And if so, can I get away with sequential or do I need full-blown parallel? So, not only you, but your team will be thankful if you do
minimum code needed to get there, especially when things are enormously complex that you can get to. Again, I'm not trying to scare you away from complexity. That's not my goal. My my goal here is to be realistic. Because it requires a lot of effort, your time is precious, and you want to make sure where you're spending the time actually provides value to your business. If the business
doesn't gain a whole lot, then we did not serve the business. We served our own desire to tinker with code and bring in complexity, and that's what I have the evening hours for. I just play with code to my heart's desire. I don't have an impact on production code in that case, and it I get to learn from that as well. So, please use your judgment. Uh
so, what I gave here is an example and and this one is from a book I published last year on Cruising Along with Java, where I talk about, among other things, about gatherers as well. And And this is something that you can focus on and and take a look at it. And And if you're really interested, since I see a few people with the cameras, I'll I'll
make this one better. You can take a beautiful picture of this beautiful boat as well. And And that's basically the the Cruising Along with Java. Oh, let me see if I can bring that here. So, this the book I was talking about. Uh hope that was useful. That's all I have. >> [music]
More from this event
See all 126 talks →
AI Is Not the Risk. Architectural Drift Is - Sunil Kalkunte
17:39
Breaking the Monolith: Tesco’s Journey to Federated GraphQL with xAPI - Vishwas Chandrashekar
29:13
A Practical Introduction to LangChain4j - Venkat Subramaniam
1:01:28
Beyond the AI Models: How Lowe’s is Building the Store That Knows - Swaroop Shivaram
13:59