vlambda博客
学习文章列表

函数式编程: 在 JS 中创建更好的循环

Functional Programming: Create Better Loops in JS

函数式编程: 在 JS 中创建更好的循环

Working with loops is quite common in procedural programming. But, applying some Functional Programming (FP) techniques may lead to better, clearer coding. In this article, we’re going to discuss common loops, and how they may benefit from giving an FP-look to them.

在程序编程中使用循环是很常见的。但是,应用一些函数式编程(FP)技术可能会导致更好、更清晰的编码。在本文中,我们将讨论公共循环,以及它们如何从提供 FP-look 中受益。

Functional-style loops

函数式样式的循环

Modern JavaScript provides several ways of working with implicit loops. For instance, applying some logic to all elements of an array may be done with [.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map ".map()") to produce a new array as a function of the original one, or [.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce ".reduce()") to produce a result out of all the array elements. There are even more[1]: some(), every(), filter(), find(), etc., all of which perform loops implicitly.

现代 JavaScript 提供了几种处理隐式循环的方法。例如,对数组的所有元素应用一些逻辑可以通过。Map ()生成一个新数组作为原数组的函数,或者。Reduce ()生成所有数组元素的结果。还有更多: some ()、 every ()、 filter ()、 find ()等等,它们都隐式地执行循环。

These methods are quite common in FP. Also, they are to be preferred because they allow you to program in shorter, more declarative ways. For instance, if you read someArray.map(v => aFunction(v)) you already know that the developer wants to apply a certain function to every element in some array to generate a new, transformed one. (A short aside: FP coders would probably write the sample code above as someArray.map(aFunction) — check our previous article on Pointfree Style Programming[2] for more on this.) Of course, you still have to understand what the mapping function does. But, you won’t have to worry if the loop limits are correct, if there are “off-by-one” bug[3], if the function is applied correctly, etc.

这些方法在 FP 中很常见。此外,它们是首选的,因为它们允许您以更短的、更声明性的方式进行编程。例如,如果您阅读了 someArray.map (v = > aFunction (v)) ,那么您已经知道开发人员希望将某个函数应用于某个数组中的每个元素,以生成一个新的、经过转换的元素。(顺便说一句: FP 编码人员可能会把上面的示例代码写成 someArray.map (aFunction)ーー查看我们上一篇关于 Pointfree Style Programming 的文章以获得更多信息。)当然,您仍然需要理解映射函数的作用。但是,您不必担心循环限制是否正确,是否存在“ off-by-one”错误,是否正确应用了函数等等。

When these operations aren’t appropriate, you still can resort to more general .forEach() loops.

当这些操作不合适时,您仍然可以使用更通用的.forEach ()循环。

Along the same lines of the previous example, if you see code like someArray.forEach(v => doSomething(v)) you know that it will be doing something for each and every element of the array, from the first to the last.

如果您看到类似于 somaearray.foreach (v = > doSomething (v))的代码,那么您就知道它将为数组的每个元素执行某些操作,从第一个元素到最后一个元素。

It’s possibly not so clear what it’s doing. It’s obvious that there is some kind of side effect — as we discussed in another previous entry in this series[4] — because otherwise the code wouldn’t do anything at all — but what? But still, using .forEach() allows us to be sure that no element in the array will be missed, or that the code won’t attempt to access elements beyond the last one, or similar common bugs.

可能还不清楚它在做什么。很明显,这里有某种副作用(正如我们在本系列的另一篇文章中所讨论的) ,因为否则代码根本不会做任何事情,但是什么呢?尽管如此,使用。forEach ()允许我们确保数组中不会遗漏任何元素,或者代码不会尝试访问上一个元素以外的元素,或者类似的常见错误。

All the loops in this section are, then, good in practice. But they aren’t the more common type of loop!

因此,本节中的所有循环在实践中都是很好的,但它们不是更常见的循环类型!

Procedural-style loops

过程式样式的循环

Programming is usually taught in imperative style[5] so it’s fairly common to see general loops written like the following.

编程通常采用命令式教学,因此常见的一般循环编写方式如下。

for (let i=9; i<=22; i++) {  
// do something with i
}

Old-fashioned developers may even eschew this kind of loop, and go for a while construct!

老式的开发人员甚至可能会避开这种循环,转而使用 while 构造!

let i = 9;  
while (i <= 22) {
// do something with i
i++;
}

With enough experience, you can recognize these patterns and understand the implied process. In these loops we want to do something to every value in the range from 9 to 22 inclusive. However, we may make some points.

有了足够的经验,您就可以识别这些模式并理解其中隐含的过程。在这些循环中,我们希望对9到22之间的每个值都执行某些操作。然而,我们可以提出一些观点。

  • Off-by-one bugs are more probable. For example, you might write i<22 instead, and then the loop would end at 21, not 22.
  • 更有可能是一个一个的错误。例如,您可以改写 i < 22,然后循环将在21而不是22结束。
  • If you happen to modify the index variable i, the loop may go on forever or suddenly stop.
  • 如果您碰巧修改了 index 变量 i,那么循环可能会永远继续下去,也可能会突然停止。
  • Code is longer (more lines) than with the functional-style loops.
  • 代码比函数样式的循环更长(更多行)。

So, can we do better with some FP-style code? Let’s see how!

那么,我们可以用一些 fp 风格的代码做得更好吗? 让我们看看如何做!

Functional-style ranges and looping

函数样式的范围和循环

We have to figure out how we want to work. If we had a function that produced an array with the values to process, then we could rewrite the loop as here.

我们必须弄清楚我们想要如何工作。如果我们有一个函数生成一个数组,其中包含要处理的值,那么我们可以像这里一样重写循环。

range(9, 22).forEach(i => {   
/* do something with i */
})

Our range() function would have from and to parameters, and generate an array with values starting at from and ending at end. A first version could be as follows.

我们的 range ()函数将具有 from 和 to 参数,并生成一个值从开始到结束的数组。第一个版本可以如下。

const range = (from, to) => {  
const arr = [];
do {
arr.push(from);
from++;
} while (to >= from);
return arr;
};

This works well: range(9,22) produces an array [9, 10, 11, … 22] as desired. A call with equal arguments also works: range(60,60) gets [60]. There’s a missing case: what about a reverse range, like range(12,4)? We could then also want fractional steps, so a generalization is in order.

这个工作得很好: range (9,22)根据需要生成一个数组[9,10,11,... 22]。具有相等参数的调用也可以工作: range (60,60) gets [60]。还有一个缺失的情况: 反向范围如何,比如范围(12,4) ?我们还可以使用小数步骤,这样就可以进行一般化。

const range = (from, to, step = Math.sign(to - from)) => {  
const arr = [];
do {
arr.push(from);
from += step;
} while ((step > 0 && to >= from) || (step < 0 && to <= from));
return arr;
};

We’re done! This version handles all kinds of ranges, and you can even do something like range(2, 4, 0.6) and get [2, 2.6, 3.2, 3.8]. You may even skip providing the loop step, and everything will still work. However, there are some common loop features that we lack… What about breaking out[6], or skipping ahead[7]? Let’s see if we can do anything about those.

我们完了!这个版本可以处理各种范围,您甚至可以执行范围(2,4,0.6)并获得[2,2.6,3.2,3.8]。你甚至可以跳过提供循环步骤,一切仍然可以正常工作。然而,有一些常见的循环特性是我们所缺乏的... ... 那么突破或者跳过这些特性呢?让我们看看我们是否可以做些什么。

Jumping around

跳来跳去

Documentation for[8] [forEach()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach "forEach()") loops[9] is clear: “There is no way to stop or break a forEach() loop other than by throwing an exception. If you need such behavior, the forEach() method is the wrong tool.” There’s no simple way to emulate a break or continue. Throwing an exception (and catching it out of the loop) would do for a break, but it’s a bit wordy, and not really the way to use exceptions!

forEach ()循环的文档很清楚: “除了抛出异常,没有其他方法可以停止或中断 forEach ()循环。如果您需要这样的行为,那么 forEach ()方法是错误的工具。”没有简单的方法来模拟间断或继续。抛出异常(并在循环外捕获异常)可以暂时中断,但有点罗嗦,而且不是真正使用异常的方法!

There’s another “dirty trick”: we could use [.some(fn)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some ".some(fn)") instead of .forEach(fn). As long as the fn function returns a falsy value, the loop will continue; when it returns a truthy value, the loop would end. We could thus write code as shown below — but don’t, please wait!

还有一个“肮脏的把戏”: 我们可以用。一些(fn)而不是。forEach (fn).只要 fn 函数返回一个虚值,循环就会继续; 当它返回一个真值时,循环就会结束。因此,我们可以编写如下所示的代码ー但是不要,请等待!

range(9, 22).some(i =>; {  
...
...
if (someCondition) return false; // <-- ...="" a="" break="" code="" continue="" if="" return="" somethingelse="" true="">

Is this good code? It works, sure, but we’re using .some() in an unintended way. Readers of the code would just become frustrated — and understanding those special return statements is not so easy. No, we need a better solution, and fortunately JavaScript has the right tool: generators[10].

这是好的代码吗?当然,这是可行的,但是我们正在使用。以一种意想不到的方式。代码的读者只会感到沮丧ーー理解这些特殊的 return 语句并不容易。不,我们需要一个更好的解决方案,幸运的是 JavaScript 有正确的工具: 生成器。

Generating ranges

生成范围

Generators are functions that can be exited but also re-entered at a later time. Each time you call a generator it will yield (return) a value, or it won’t return anything signalling that there are no more values. Code for our range() solution would be as follows. In particular, note the function* declaration, which implies a generator function.

生成器是可以退出但在稍后时间重新进入的函数。每次调用生成器时,它都会产生(返回)一个值,或者它不会返回任何表示没有更多值的信号。我们的 range ()解决方案的代码如下。特别要注意函数 * 声明,它暗示一个生成器函数。

function* range(from, to, step = Math.sign(to - from)) {  
do {
yield from;
from += step;
} while ((step > 0 && to >= from) || (step < 0 && to <= from));
}

Generators cannot be used with .forEach(), but they work with for..of statements. We should now write code as follows.

生成器不能与.forEach ()一起使用,但可以与. . of 语句一起使用。

for (const i of range(9, 22)) { i => {   
/* do something with i */
}
}

Quite clear! You may also directly use break and continue statements.

非常清楚! 您也可以直接使用 break 和 continue 语句。

for (const i of range(9, 22)) { i => {   
...
...
if (someCondition) continue;
...
if (somethingElse) break;
...
...
}
}

Oh, and if you actually wanted an array, you may spread out the generator results as follows.

哦,如果您实际上想要一个数组,您可以将生成器的结果分布如下。

const arrayFrom9To22 = [...range(9, 22)];  
// this produces [9, 10, 11, ... 22]

Finally, by using generators we gain a tad of performance, because we won’t be (first) generating an array, and only then (second) processing it. You may even work with truly large ranges without requiring proportionate memory: range(9,999999999999) won’t cause any memory problems!

最后,通过使用生成器,我们获得了一点性能,因为我们不会(首先)生成一个数组,然后(第二)处理它。您甚至可以在不需要相称内存的情况下使用真正的大范围: 范围(9,9999999999999)不会引起任何内存问题!

Summary

摘要

We have seen how to make common loops clearer by using a range-producing function, which allows us to write code more succinctly. Our first solution (producing arrays) was simple but had some limitations because it didn’t allow us to use common statements to break out of loops, for example. The final solution we considered, using generator functions, was free from those limitations and also had better performance. Get used to writing code in this fashion, and your code will be both shorter and clearer.

我们已经看到如何通过使用范围生成函数使公共循环更加清晰,这使我们能够更简洁地编写代码。我们的第一个解决方案(生成数组)很简单,但是有一些限制,例如,它不允许我们使用公共语句打破循环。我们考虑的最终解决方案是使用生成器函数,它不受这些限制,并且具有更好的性能。习惯以这种方式编写代码,你的代码就会更短更清晰。

参考资料

[1]

even more: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array

[2]

previous article on Pointfree Style Programming: https://medium.com/stackanatomy/forever-functional-pointfree-style-programming-c3877c229ceb

[3]

“off-by-one” bug: https://en.wikipedia.org/wiki/Off-by-one_error

[4]

another previous entry in this series: https://medium.com/stackanatomy/forever-functional-injecting-for-purity-eb4997821510

[5]

imperative style: https://en.wikipedia.org/wiki/Imperative_programming

[6]

2, 2.6, 3.2, 3.8]. You may even skip providing the loop step, and everything will still work. However, there are some common loop features that we lack… What about [breaking out: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break

[7]

skipping ahead: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/continue

[8]

for: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

[9]

loops: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

[10]

generators: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator