Hi Habr! I suggest you articles Rethinking JavaScript: Replace break by going functional .
In my previous article, Rethinking JavaScript: Death of the For Loop (there is a translation: Rethinking JavaScript: Death for ), I tried to convince you to abandon for in favor of a functional approach. And you asked a good question "What about a break?".
break is goto cycles and should be avoided
We should abandon the break
as well, as we once abandoned GOTO
.
You might think, "Come on, Joe, you're exaggerating. How does this break this GOTO
?"
// . ! outer: for (var i in outerList) { inner: for (var j in innerList) { break outer; } }
Consider the labels to prove the statement. In other languages, tags are paired with GOTO
. In JavaScript, tags work together with break
and continue
, which brings the latter together with GOTO
.
JavaScript label, break and continue is a relic of GOTO and unstructured programming.
"But break doesn't bother anyone, why not leave the opportunity to use it?"
This may sound counterintuitive, but constraints are a good thing. The GOTO
ban is a great example. We are also happy to limit ourselves to the "use strict" directive, and sometimes even condemn those who ignore it.
“Constraints can make things better. Much better.” - Charles Scalfany
Constraints make us write better.
I will not lie. There is no simple and quick way to replace break. It requires a completely different programming style. Completely different style of thinking. Functional style of thinking .
The good news is that there are many libraries and tools that can help us, such as Lodash , Ramda , lazy.js , recursion, and others.
For example, we have a collection of cats and a function isKitten
:
const cats = [ { name: 'Mojo', months: 84 }, { name: 'Mao-Mao', months: 34 }, { name: 'Waffles', months: 4 }, { name: 'Pickles', months: 6 } ] const isKitten = cat => cat.months < 7
Let's start with the good old for
loop. We will iterate our cats and exit the cycle when we find the first kitten.
var firstKitten for (var i = 0; i < cats.length; i++) { if (isKitten(cats[i])) { firstKitten = cats[i] break } }
Compare with a similar lodash option.
const firstKitten = _.find(cats, isKitten)
This was a pretty simple example, let's try something more serious. We will sort out our cats until we find 5 kittens.
var first5Kittens = [] // for for (var i = 0; i < cats.length; i++) { if (isKitten(cats[i])) { first5Kittens.push(cats[i]) if (first5Kittens.length >= 5) { break } } }
Note translator : I allowed myself some liberty and added thoughts about the easy way with missing parts, in my opinion.
We can use standard JavaScript array methods.
const result = cats.filter(isKitten) .slice(0, 5);
But it is not very functional. We can use Lodash.
const result = _.take(_.filter(cats, isKitten), 5)
This is a good enough solution while you are looking for kittens in a small collection of cats.
Lodash is great and can do a lot of good things, but now we need something more specific. Here lazy.js will help us . He is "Like underscore, but lazy." We need his laziness.
const result = Lazy(cats) .filter(isKitten) .take(5)
The fact is that lazy sequences (which lazy.js provides) will make exactly as many conversions (filter, map, etc.) as many elements you want to get at the end.
Libraries are fun, but sometimes it 's really fun to do something yourself!
How about creating a generic (approx. Generic) function that will work as a filter
, but in addition will be able to stop when finding a certain number of elements?
First we wrap our good old cycle into a function.
const get5Kittens = () => { const newList = [] // for for (var i = 0; i < cats.length; i++) { if (isKitten(cats[i])) { newList.push(cats[i]) if (newList.length >= 5) { break } } } return newList }
Now let's summarize the function and render all the co-specific. Replace 5
with limit
, isKitten
with predicate
and cats
with list
and put them into function parameters.
const takeFirst = (limit, predicate, list) => { const newList = [] for (var i = 0; i < list.length; i++) { if (predicate(list[i])) { newList.push(list[i]) if (newList.length >= limit) { break } } } return newList }
As a result, we have the takeFirst
function ready for reuse, which is completely separate from our cat logic business!
takeFirst
is a pure function . The result of its execution is determined only by input parameters. The function is guaranteed to return the same result by getting the same parameters.
The function still contains the nasty for
, so continue the refactoring. The next step is to move i
and newList
to the function parameters.
const takeFirst = (limit, predicate, list, i = 0, newList = []) => { // ... }
We want to end the recursion ( isDone
) when limit
reaches 0
( limit
will decrease during recursion) or when the list
ends.
If we are not finished, we execute the predicate
. If the result of the predicate
is true, we call takeFirst
, decrease the limit
and attach the element to newList
.
Otherwise, take the next item in the list.
const takeFirst = (limit, predicate, list, i = 0, newList = []) => { const isDone = limit <= 0 || i >= list.length const isMatch = isDone ? undefined : predicate(list[i]) if (isDone) { return newList } else if (isMatch) { return takeFirst(limit - 1, predicate, list, i + 1, [...newList, list[i]]) } else { return takeFirst(limit, predicate, list, i + 1, newList) } }
Our last step to replace the if
with the ternary operator is explained in my article Rethinking Javascript: the If Statement .
/* * takeFirst `filter`, . * * @param {number} limit - * @param {function} predicate - , item true false * @param {array} list - , * @param {number} [i] - , ( 0) */ const takeFirst = (limit, predicate, list, i = 0, newList = []) => { const isDone = limit <= 0 || i >= list.length const isMatch = isDone ? undefined : predicate(list[i]) return isDone ? newList : isMatch ? takeFirst(limit - 1, predicate, list, i + 1, [...newList, list[i]]) : takeFirst(limit, predicate, list, i + 1, newList) }
Now let's call our new method:
const first5Kittens = takeFirst(5, isKitten, cats)
To make takeFirst
more useful, we could curry it and use it to create other functions. (more about currying in another article)
const first5 = takeFirst(5) const getFirst5Kittens = first5(isKitten) const first5Kittens = getFirst5Kittens(cats)
There are many good libraries (for example, lodash, ramda, lazy.js), but being brave enough, we can use the power of recursion to create our own solutions!
I have to warn you that while takeFirst
incredibly cool, with recursion comes great power, but also a lot of responsibility . Recursion in the JavaScript world can be very dangerous and easily lead to a stack overflow error Maximum call stack size exceeded
.
I will talk about recursion in javascript in the next article.
I know that this is a trifle, but I am very happy when someone subscribes to me on Medium and Twitter @joelnet . If you think I'm a fool, tell me in the comments below.
→ Functional JavaScript: Functional Composition For Every Day Use.
→ Rethinking JavaScript: Death of the Loop
(there is a translation: Rethinking JavaScript: Death for )
→ Rethinking JavaScript: Elliminate the switch statement for better code
→ Functional JavaScript: Resolving Promises Sequentially
Note translator : I thank Gleb Fokin and Bogdan Dobrovolsky in writing the translation, as well as Joe Toms , without whom translation would be impossible.
Source: https://habr.com/ru/post/330082/
All Articles