📜 ⬆️ ⬇️

FRP (functional reactive programming) on ​​Bacon.js

Often, when creating quite complex JavaScript applications, the moment comes when it becomes completely incomprehensible why the application stopped working as it should, or, on the contrary, it suddenly worked. There are so many connections between the elements of the application that it is very difficult to keep track of them even with good debuggers. And here's the dilemma: on the one hand, there is a well-known method of creating applications on JS, so familiar and deeply described that we already don’t notice the shortcomings. On the other hand, there are lots of libraries offering us to switch to the other side to try something new. These libraries include Bacon.js, providing an FRP implementation in JavaScript.

A few words about FRP and its applied sense.
If you do not go into Tao functional programming, then you can select a few points FRP particularly attractive for web development. It:

With states, of course, not everything is so transparent. In the end, it's still JavaScript with its inherent problems. So somewhere under the hood of the browser, the same thing happens all the same, which would have happened in the code without bacon.js, but the whole thing is that this is no longer the concern of the developer. The task of the developer is to think about the logic of the application.

FRP implies that data sources are event streams. The data itself is a state of the stream at a certain time, so a change in the data entails an immediate change in other data that depends on the first. The result is a structure tree that describes the dependence of some data on others, which makes the system transparent and easily digestible.

Now I would like to turn to the applied part, namely, by example, to demonstrate the advantages of FRP in general and Bacon.js in particular over imperative JS programming. As an example I will take the well-known game Sokoban , which I wrote using Bacon.js and without, in order to demonstrate the differences.
')
In order not to go into excessive detail, you can immediately look at the result . I will focus on the functional part of the game, the one that is responsible for the logic of action. If someone has an overwhelming desire to get acquainted with raw materials, then please . In a nutshell, everything works like this: we set the width and height of the playing field in cells (DHTML), draw a level (JSON object) and use the arrows to move the player (block with a green background) across the field, trying to move the yellow cells to blue when all yellow cells are in blue - level passed.

Practical use
And now to the most important thing. Usually, we simply listen to the events that the browser generates in response to the actions of the user, to find out what to do next.

$(document).on("keydown", function(e){ //     }); 

The game is controlled by 4 keys. And then immediately there is the problem of conditional verification, which syntactically looks like this.

 if(e.keyCode >= 37 && e.keyCode <= 40){ //  ,    } 


We did not really have time to start anything, but we already have to work within the framework of this very condition. For example, to calculate the direction of motion. As we move deeper into the logical structure of the game, we are faced with numerous environmental checks, the sole purpose of which is to determine the current implicit state.

What does bacon.js offer? In bacon.js, streams (EventStream) and properties (Property) are defined - the stream states at a certain time. Streams can be processed, combined, combined. There are visual diagrams of methods. Thus, the description of the necessary reactions is reduced to the description of the order of events in the stream and data conversion. The idea is not to follow the single events and process each of them separately, but to get the data source, that is, the stream of events from which you can extract only the necessary ones or convert them. For example using filters:

 var keyDowns = $(document).asEventStream("keydown"); //  keydown  $(document) var arrowDowns = keyDowns.filter(isArrows); // , ,    isArrows function isArrows(e){ // asEventStream     jQuery.Event return e.keyCode >= 37 && e.keyCode <= 40 } 

Or using the map:

 var changeDirection = $(document).asEventStream("keydown") .filter(isArrows) // filter  true,    ,    .map(selectDirection) //map   selectDirection(event) .onValue(function(x){ //   ,    //     }); function selectDirection(e){ return { x : e.keyCode % 2 ? e.keyCode - 38 : 0, y : !(e.keyCode % 2) ? e.keyCode - 39 : 0 } } 

Or something else. There are many methods for working with threads and properties. The point is not even what functionality Bacon.js provides, but what you can do with these event streams later. If the general algorithm of the game without bacon.js is a cobbled labyrinth of conditions and states, then with the help of FRP we achieve quite a declarative description of the program states and data sources.
We are used to changing the state of the system when the necessary events occur, whether it is keyboard input or a click on an object. To understand that the events you need to hang up handlers and closely monitor developments. We have to plan a lot and predict how the user will behave in general, so that every state has its own actions. But in reality, as a developer, I have little interest in what the user did, if it does not logically affect the state of the system. With this approach, a declarative description of events makes life much easier because it does not require checks in the style of “but it’s true that the user poked this particular button on the keyboard and not some other one”.

 var playerMove = $(document).asEventStream("keydown") // keydown  $(document) .filter(isArrows) //     .map(player) //  .map(nextCell); //,   . // , ,        var playerNextEmpty = playerMove.filter(isEmpty).onValue(function(nov){ //  . }); // , ,       -   var goalMove = playerMove.map(isGoal).filter(function(x){return x}); // , ,     ,     -  var goalNextEmpty = goalMove.map(nextCell).filter(isEmpty).onValue(function(x){ //   }); 

As you can see, the syntax itself pushes to write obviously. In fact, everything can be made even easier if you use the scan () and combine () methods, but I wanted to show the simplest methods map () and filter ().

The same functionality without bacon.js will not be brought in its entirety, but the callback that handles the game mechanics at a keydown event ends with this:


The declarative approach simplifies the task when you need to supplement the system with new elements with new functionality. For example, add mines to the field. If a player turns out to be on a mine or pushes a block from him, the game will be considered lost.

 // , ,   ,    -  var playerNextMine = playerMove.filter(isMine); // , ,   ,     -  var goalNextMine = goalMove.map(nextCell).filter(isMine); //   playerNextMine  goalNextMine. var mineAlert = goalNextMine.merge(playerNextMine).onValue(function(x){ //  }); 


It would be much more difficult to add exactly the same functionality in the imperative style. In a confusing system of conditional checks and identification of states, it would be necessary to embed another possible scenario. Think about how other scenarios are combined with this scenario, whether there are conflicts and so on.

Advantages and disadvantages




Does it all make sense?
From all written above we can conclude. There is a hypothetical curve of the need to use bacon.js (and generally FRP) on the complexity of the application. It is clear that for targeting the beauty to the site or for the sign form alone there is no point in using bacon.js. The library is not heavy, but it’s better to see right away where you can apply it, and where you just don’t. Bacon.js is wise to use where it will be difficult to navigate without declarative programming. Even if the system is large and includes a huge number of elements, it makes sense to use bacon only when these elements are dependent on each other, changing some data should entail changing other data, and so on. The complexity in this sense does not mean the size of the application or the total complexity of the algorithms, rather the size of the logical structure of the application.

Alternatives
From JS libraries there is Microsoft RxJS . There is such a thing as Elm . There is such an interesting thing as ClojureScript . Having devoted a little time to studying this issue, you can convenient option.

Materials :
GitHub: Bacon.js , web-site
A few good FRP materials: one , two , three .

Source: https://habr.com/ru/post/198656/


All Articles