⬆️ ⬇️

What is wrong with for loops?

The possible appearance of closures in Java has become a hot topic for discussion. A proposal is being prepared to add closures to the upcoming version of the language, but the proposed syntax, coupled with the complexity of the language, is being criticized by many Java programmers.



Today, Elliotte Rusty Harold has published his doubts about possible changes. One of the main questions he asked was: “Why do you hate the for loop” ( “Why Hate the for Loop?” )?



I don't know what makes people dream so passionately about getting rid of for loops. This is not the first or even the second time when theorists from the computer science world rebel against them.


It would be wrong to think that Elliott alone questions the need for poor and unhappy closures. Mostly, he complains that he didn’t see after reading Bruce Tate’s recent article (Bruce Tate) any value in possibly introducing closures in Java (the latter uses Ruby for examples):



Listing 1. The simplest closure.

3.times {puts "Inside the times method."} 


Results:

Inside the times method.

Inside the times method.

Inside the times method.

')

Here times is the method of object 3, it executes the closure code three times.

 {puts "Inside the times method."} 
and there is a closure. This is an anonymous function that, when passed to the times method, prints the string “Inside the times method.” To print. This code is much clearer and simpler than its alternative using the for loop:



Listing 2. A loop without closures.

 for i in 1..3 puts "Inside the times method." end 


Even I, seeing such a pale and lifeless introduction to closures, would hardly see their real meaning. The first thing that comes to mind at the sight of these examples: "Yes, perhaps there is some subtle tint here." The remaining examples from Bruce's article are just as trivial, unclear, and do not shed any light on the subject of discussion at all.



I will not discuss Elliot’s claims for code written in Ruby — this topic is not worth a damn. I will also get round the debate about the syntax proposed for closures in Java, and the question of whether they fit into this language at the current stage of its development. I am not interested in this issue, and, frankly, it does not matter to me when and how these problems will be resolved and whether they will be resolved at all.



However, Elliott raises a really important question: “Why do you hate the for loop”?



Here is a common example:



 double sum = 0; for (int i = 0; i < array.length; i++) { sum += array[i]; } 


What is he doing? I have been programming for many years, and understanding a piece of this code does not take me much time - this is an ordinary summation of array elements. However, in order to read this example, I have to sequentially look at about thirty lexemes sprayed in four lines. Of course, when writing this code one could use syntactic sugar, which would reduce its volume. However, the point is that there are many places where you can potentially make a mistake when writing ordinary summations.



Do you want evidence? Please give you the following example from the Elliott article:



 String s = ""; for (int i = 0; i < args.length; i++) { s += array[i]; } 


Notice a bug? Even if this code is compiled and revisited, weeks may pass before the first error message is received and several more before the release of the fix. And these are just simple for loops. And now imagine how much more difficult the work becomes in the case of strongly overgrown, and, perhaps, nested cycles. (If you are still not worried about bugs, think about common typos. And then think about how often you write such cycles.)



If you had the opportunity to write a simple for loop in one line, with fewer characters, it would be much easier to read. Because a shorter form of writing leads to much less likelihood of errors in the code, and when they do appear, they are (mostly) much easier to catch.



How could closures help in this situation? Here is the first Haskell example:



 total = sum array 


Well, okay, okay, it was not entirely fair. The sum function does not use closures — it is defined through convolution (note of the translator: the English version is fold), which, in turn, uses closures.



 total = foldl (+) 0 array 


Here is the second example of applying closures:



 s = concat array s = foldr (++) [] array 


Perhaps the example of using the strange-looking functions foldl and foldr to explain closures is not so obvious to programmers who are familiar only with cyclic constructions. However, he emphasizes the main weakness of for loops — they try to combine three different operations — filtering, folding, and converting.



The two examples above show how you can take a list and collapse it into one item. In the world of functional programming, such operations are called convolutions. For its work, convolution takes the function (closure), the initial value (approx. Translator: hereinafter referred to as “accumulator”) and visits the first element of the list. The closure is applied to the battery and the first item in the list, the result is put into the battery. The convolution then applies the closure to the updated accumulator value and the second list item. This process continues to the end of the list when the last value of the accumulator is the result of the convolution.



Here is a visual explanation:



 s = foldl (+) 0 [1, 2, 3] = foldl (+) (0 + 1) [2, 3] = foldl (+) 1 [2, 3] = foldl (+) (1 + 2) [3] = foldl (+) 3 [3] = foldl (+) (3 + 3) [] = foldl (+) 6 [] = 6 


Haskell provides several convolution functions: foldl starts processing the list from its first element, and passes sequentially to the last element; foldr, on the contrary, begins with the last element, and moves towards the first element. There are other options, but these two are major.



Of course, convolutions are very primitive operations, and it would be very unwise to throw the for loops overboard and replace them with various fold-shaped “spells”. Instead, higher-level operations, such as sum, prod, and concat, are defined in terms of convolutions. You, in turn, program in terms of these high-level operations, getting more concise code, code that is much easier to read, write and understand.



However, not every cycle is a folding of the list into one element. Consider the following example:



 for (int i = 0; i < array.length; i++) { array[i] *= 2; } 


In this case, we are dealing with a transformation, in functional languages ​​this is called a map:



 new_array = map (*2) array 


The map function visits each element of the list, applies the closure passed to it to the [element] and creates a new list from the values ​​returned by the closure. (Some languages ​​instead of creating a new list replace the old elements of the original list with new ones). Such an operation is much easier to understand. sort does something similar, in the sense that it takes a list and returns a new list (or modifies the old one).



The third type of for loop is the filter loop. Consider an example:



 int tmp[] = new int [nums.length]; int j = 0; for (int i = 0; i < nums.length; i++) { if ((nums[i] % 2) == 1) { tmp[j] = nums[i]; j++; } } 


Very simple code, the meaning of which, nevertheless, is almost completely buried under the unnecessary complexity, which led to the use of a for loop and two indices. If filtering is the same fundamental operation as fold and map, applying it should be just as simple, and, generally speaking, it is:



 odds = filter (\i -> (i `mod` 2) == 1) nums odds = filter odd nums --   


Here, strictly speaking, all the shortcomings of the for loop: it combines (at least) three types of operations, and focuses attention on a secondary detail - the passage of a sequence (indices) of values. But in fact, minimizing, transforming, and filtering are three different ways to handle the list and they should be perceived differently. The use of closures in the body of the cycle allows a much clearer to separate what from how . I could write anonymous functions whenever I need to process a list, or use functions written before me (for example, odd, (+) or sqrt).



If closures are not an esoteric tool, but are deeply embedded in the language and its standard library, we do not need to bother with these low-level operations (note of the translator: here we mean map, fold and filter). Instead, we can create high-level operations with talking names, such as sum or product.



In addition, working with such terms makes it much easier to understand more complex operations, such as transforming a tree (mapping a tree), filtering a vector (filtering a vector), or folding a list into a hash (note of the translator: it’s not quite clear what I meant the author under the words “folding a list into a hash” - folding the list into a single value, or the function of obtaining a hash table from a list of tuples, available in the standard library).



In the end, Elliott threw up his hands on the topic of parallel execution of code on several cores, and complained that

 3.times {...} 
will almost certainly be worse than a similar for loop. It seems to me that he misses one thing. Yes, there are calculations that must be performed in series, there are also those that can be performed in parallel. But separating one from the other is difficult for the compiler if the only tool you use is a for loop. If you can separate sequential calculations (that is, foldl and foldr) and (possibly) parallel (map and filter), then, by this, you seriously simplify the task of the compiler. Moreover, you can even explicitly request a serial or parallel version of map if you know the nature of the data you are processing better than the compiler.



From translator



In the text, references to closures often slip. If I correctly understand the definition of closures , then I must say that the author confuses anonymous functions and closures - in the examples he cites there are no functions that work with variables from the environment that are not listed in the parameter list.

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



All Articles