Imperative and reactive programing are two separate programing paradigms. But what is the difference, and how do they correlate with functional programing?
To highlight the differences let's see a code snippet that we will use to compare the two paradigms. If we suppose below that the
items are numbers then we can assume, that this code first drops the odd numbers then adds half to the remaining numbers and finally it logs the results to the console.
const newItems = items .filter(item => item % 2 === 0) .map(item => item + 0.5); newItems.forEach(item => console.log(item) );
The code above clearly uses functional programming when it comes to filtering and mapping the original items to new items, but can we also say it's imperative or reactive? And if we know that these two things are quite different then which one is the above snippet? Well, it depends on the context.
Imperative programing is the way you most likely write your code every day. The previous functional programing code can be interpreted as imperative if we look at it in a more abstract way and say that the whole filtering, mapping and value assignment part is a statement. Imperative programing basically means that the code gets executed in order statement by statement.
The order still can mean that you are using conditionals, loops or function calls making the compiler jump to different parts of your code, but if your debugger can clearly point to a statement in your code and it's also clear what might be the next line where it jumps to, then most likely you are using imperative programing.
So let's add some context to our previous code, and let's see what gets written to the console and when does it do that?
let items = ; items.push(1); items.push(2); items.push(3); items.push(4); const newItems = items .filter(item => item % 2 === 0) .map(item => item + 0.5); newItems.forEach(item => console.log(item) ); // Output: 2.5 // Output: 4.5 items.push(5); items.push(6); items.push(7); items.push(8);
If we read this code line by line then we can say that:
- we create an array,
- add some values to it,
- then create a new array which is the modified version of the original one,
- console log the new array's items,
- and finally it adds a few more values to the original array.
Most probably you are not surprised by the result, the values gets logged when the console.log statements get executed. Once that statements are finished the lastly added few values do not affect logging because they happen after the logging statements got executed (and they are not being added to the logged array anyway).
So we said that our first code snippet can be interpreted as imperative. Knowing that reactive programing is a completely different world, can we still say that our first code snippet is reactive? Let's see it in a different but quite similarly looking context.
import Rx from 'rxjs/Rx'; let items = new Rx.Subject(); items.onNext(1); items.onNext(2); items.onNext(3); items.onNext(4); const newItems = items .filter(item => item % 2 === 0) .map(item => item + 0.5); newItems.forEach(item => console.log(item) ); items.onNext(5); items.onNext(6); // Output: 6.5 items.onNext(7); items.onNext(8); // Output: 8.5
Well, that might be new. We add things to the original
items and it gets transformed and logged by the
newItems which got defined before even adding the item that get's logged.
Reactive programming is programming with asynchronous data streams that can be created, changed or combined on the go. The trick is that we are not working with arrays now but streams. To explain that let's have a car analogy.
Arrays are the parking lots that can permanently store cars. When we check the parking lot, we can see all the cars that are presented there. When we log each then we can log each car currently staying in the parking log.
Streams are the streets where cars are present but just passing by, they are not really stored there. When we look to the street we only see the car just passing by. So when we log each it wouldn't really make sense to only log the car which is currently passing in front of us. The logging in this case is a continuous observation that happens once the stream is defined.
But streams can do more than that. If the driver is a wanted criminal then the police might stop him, and the car basically gets filtered from the stream. A car wash can also be a stream. Cars still go through it but they get cleaned during the process (see that map function over there?).
So the above code can be read as follows:
- We create a data stream
- We add some data to the stream which won't be logged because no one is listening
- We create a new stream which is an alternation of the original one (both streams keep flowing)
- New values gets added to the original stream, but as the new stream is an alternation of it the original, the new values might pop up (in an alternated way) in the new stream
- The new stream keeps flowing in parallel with the original one and every time a new items pops up, it will log the value
Want to try it out?
I created a pen to show the two different behaviours in parallel. The code writes everything to the dom instead of using console.log to make it more visible but the code is basically the same.
How can the same code behave so differently?
items.mapthen the map function is the items's own map function. So in case of an array call like
[1,2,3].mapthe map function is actually
Array.prototype.map. If items is a stream, then a completely different function gets called. But you don't have to worry about that they intentionally look the same because the idea behind them is the same.