📜 ⬆️ ⬇️

Rethinking JavaScript: break and functional approach

Hi Habr! I suggest you articles Rethinking JavaScript: Replace break by going functional .


image


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.


image


"But break doesn't bother anyone, why not leave the opportunity to use it?"


Why should you limit yourself when developing software?


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.


Why Programmers Need Limits


What are the alternatives to break?


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 } } } 

Easy way


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.


Difficult path


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) 

Results


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.


Related articles


→ 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