The task: input signals from the keyboard (keyup, keydown) - output letters and words decoded according to Morse code. On how to declaratively solve this problem using the FRP approach, in particular Rx.js - below under the cut. (Why? Because we can)
For non-patient:main idea
This demo is intended to demonstrate the power of the
Jedi reactive programming to solve the problem of the composition of asynchronous computing. Rx makes you look at asynchronous code differently, treating events as collections. A lot of articles about Rx and functional programming look like this:
')
Therefore, I will try to describe in more detail one of the functional parts of the application (with live examples and diagrams).
Morse logic
The letter in Morse code is a set of long and short signals (dots and dashes) separated by a certain time interval.
Basic rules (ideal):- Per unit of time is taken as the duration of one point.
- The duration of the dash is equal to three points.
- Pause between elements of the same sign - one point.
- Pause between characters in the word - 3 points.
- Pause between words - 7 points.
Looking ahead, I want to warn you that I didn’t bother much about the dimension and took the length of 400 ms “by eye”. This project does not claim 100% compliance with the real Morse code (do not try to use it in military conditions), but the principle of operation remains the same. With respect to 400 ms, the remaining time intervals are calculated (the size of the dashes, the pause between letters and words). The interface is constructed in such a way that it makes it clear when it expects a character (point, dash) from a letter, a new letter, or the next word.
- What? Yes, I'm on my knee in 5 minutes and without RxWhat is the main difficulty?
The main difficulty lies in the fact that we are dealing with asynchronous logic. The number of signals in the letter is non-deterministic. For example, the letter 'A' consists of two characters - a dot and a dash (.-), while a '0' is five dashes (-----). It is also not easy to imagine how to count the time from one letter to another. How between this whole thing still understand that there was an interval between words? .. This problem can be solved by a standard imperative approach with a bunch of callbacks or promises and setTimeout or newfangled async / await. I do not want to convince you that this is wrong, I just want to show another approach that I like.
Task decomposition and different layers of abstraction
- Divide et impera !!!To solve a complex problem, it is necessary to divide it into smaller and simpler subtasks and solve each of them separately. In this case, we have low-level signals at the input (DOM event), and letters and words at the output. This task can be compared with the
OSI network model. The model is represented by different levels, each of which performs its task and provides data for the higher layer. The main similarity lies in the fact that each of the levels has its own clear logic, but does not know about the whole model as a whole. Let's select the main layers of abstractions in our task:
As you can see, each layer operates with its own logic, providing the data above to the standing layer and does not know about the whole system as a whole.
Sequence of events as a first class object
Rx allows you to treat any asynchronous sequence as an object of the first class. This means we can save all arising keyup-s and keydown-s in a certain collection (Observable) and operate with it as with a regular data array.
We analyze the problem "point or dash"
Next, I will try to describe in detail the process of getting a stream in which dots or dashes will come. To begin with, we will receive collections of all keystrokes:
const keyUps = Rx.Observable.fromEvent(document, 'keyup'); const keyDowns = Rx.Observable.fromEvent(document, 'keydown');
jsfiddle exampleHaving received arrays of kyeup and keydown events, we are only interested in pressing space. We can get them using the filter operation - this will be our first level of abstraction with DOM-event-ami:
const spaceKeyUps = keyUps.filter((data) => data.keyCode === 32); const spaceKeyDowns = keyDowns.filter((data) => data.keyCode === 32);
jsfiddle exampleSchematically it looks like this:

In the picture above we see 2 streams. The top one includes all keydown events. The bottom one is based on the top one, but as you can see, the filter function has been applied to it, which filters the key code. As a result, we have a new stream with the keydown space. Have you ever seen the spaceKeyDown event in the DOM api? We have just created it based on the existing DOM event and will continue to use it.
We are not particularly interested in where the signal was received (mouse, keystroke, microphone, camera), abstracting and transmitting further just the fact that the signal started or ended:
const signalStarts = spaceKeyDowns.map(() => "start"); const signalEnds = spaceKeyUps.map(() => "end");
jsfiddle exampleBut not everything is so simple with signalStarts :)There is a small problem with the keydown event. DOM api works in such a way that the keydown event is triggered many times when the key is pressed. We can easily overcome this by adding some code:
const signalStartsRaw = spaceKeyDowns.map(() => "start"); const signalEndsRaw = spaceKeyUps.map(() => "end");
Let's see what happened here. The main problem is the occurrence of two identical successive events. In this case, you can get a common stream from start and end (signalStartsEnds) and apply the
distinctUntilChanged function to it. She will ensure that the events will not be repeated. (more about distinctUntilChanged -
here )
jsfiddle working example Next, we need to calculate the time between the beginning and the end of the signal, for this, let's add time stamps to our collections:
const signalStarts = signalStartsEnds.filter((ev) => ev === "start").timestamp(); const signalEnds = signalStartsEnds.filter((ev) => ev === "end").timestamp();
After that, you must return the time difference between keydown and keyup. Let's create a separate stream for this. Since the occurrence of keyup events is not deterministic. That is, if keydown is viewed as stream and you note the time of each keystroke, each event should return another stream that returns the first keyup value. It sounds very difficult, it's easier to see how it looks in the code:
const spanStream = signalStarts.flatMap((start) => { return signalEnds.map((end) => end.timestamp - start.timestamp).first(); });
jsfiddle exampleSchematically it looks like this:

In the image of t1, t2, t3 ... this is the time when the event occurred. t2 - t1 - time difference. You can say this as: “At each beginning of the signal, we create a stream of signals from the end of the signal, we wait for the 1st signal from it, then we transmit the time difference between the beginning and the end of the signals”. Thus, we received a stream from time intervals, and using them we can determine points and dashes:
const SPAN = 400; const dotsStream = spanStream.filter((v) => v <= SPAN).map(() => "."); const lineStream = spanStream.filter((v) => v > SPAN).map(() => "-");
The full code of the example looks like this -
an example on jsfiddle . Remove some noise and get a more beautiful
code (subjective opinion, but if you do not like it - you do not have a soul).
And here is our promised stream of dots and dashes:
const dotsAndLines = Rx.Observable.merge(dotsStream, lineStream);
Next, we operate with higher-level streams and create even higher levels of them. For example, applying some Rx transformations, we can get a stream of spaces between letters and then a letter:
const letterCodes = dotsAndLines.buffer(letterWhitespaces)
Link to the sourceBeautiful isn't it? Not? Then this is how we can display a cat image, if the user has coded the word "CAT":
const setCatImgStream = wordsStream.filter((word) => word == "CAT").map(setCatImg)
Conclusion
Thus, we see that from ordinary DOM events we have come to more meaningful things (streams with letters and words). In the future, this can be compiled further into sentences, paragraphs, books, etc. The main idea is that Rx makes it possible to compose your existing functionality to get a new one. All your logic turns into a kind of API for building a new, more complex one and that, in turn, can also be put together. In this article, I missed many more benefits that Rx out of the box gives you (testing asynchronous chains, cleaning resources, error handling). I hope I managed to interest those who wanted to try but did not dare.
Thanks for attention! All OP =)