📜 ⬆️ ⬇️

Java 8 lambdas are closures?

A detailed answer to the question put to the title of the post is given in the article of Bruce Ekkel in the edition of November 25, 2015. We decided to post here a translation of this article and ask what you think about functional programming in Java, as well as the relevance of this book:



Enjoy reading!
')


In short, of course yes.

To give a more detailed answer, let's figure it out: why are we working with them?

Abstraction behavior

In essence, lambda expressions are needed because they describe what calculations should be performed, not how to perform them. Traditionally, we worked with an external iteration, in which we clearly indicated the entire sequence of operations, as well as how they are done.

// InternalVsExternalIteration.java import java.util.*; interface Pet { void speak(); } class Rat implements Pet { public void speak() { System.out.println("Squeak!"); } } class Frog implements Pet { public void speak() { System.out.println("Ribbit!"); } } public class InternalVsExternalIteration { public static void main(String[] args) { List<Pet> pets = Arrays.asList(new Rat(), new Frog()); for(Pet p : pets) // External iteration p.speak(); pets.forEach(Pet::speak); // Internal iteration } } 


The outer iteration is performed in a for loop, and this loop exactly indicates how it is done. Such code is redundant and repeatedly reproduced in programs. However, with the forEach loop, we order the program to call speak (here - using a link to a method that is more concise than lambda) for each element, but we don’t have to describe how the cycle works. The iteration is handled internally, at the level of the forEach loop.

Such a motivation “what, and not like” in the case of lambda expressions is basic. But in order to understand closures, it is necessary to take a closer look at the motivation of functional programming itself.

Functional programming

Lambda expressions / closures are designed to simplify functional programming. Java 8 is, of course, not a functional language, but in it (as in Python) some support for functional programming is now provided, these features are built on above the basic object-oriented paradigm.
The main idea of ​​functional programming is that it is possible to create and manipulate functions, in particular, to create functions during execution. Accordingly, your program can operate not only with data, but also with functions. Imagine what opportunities open to the programmer.

There are other limitations in a purely functional programming language, in particular, data invariance. That is, you do not have variables, only immutable values. At first glance, this restriction seems excessive (how to work without variables at all?), But it turns out that, in essence, everything is achievable with the help of values ​​as with variables (you want to make sure - try Scala, this language is not a purely functional , but provides for the possibility to use values ​​everywhere). Invariant functions take arguments and produce a result without changing the environment; therefore, it is much easier to use them in parallel programming, because the invariant function does not block shared resources.
Before the release of Java 8, it was possible to create functions at runtime in only one way: generate and load bytecode (this is a rather complicated and complicated job).

For lambda expressions, the following two features are characteristic:

  1. More concise syntax when creating functions
  2. The ability to create functions at runtime; then these functions can be transferred to another code, or another code can operate with them.


The closures relate to the second possibility

What is a closure?

The closure uses variables located outside the scope of the function. In traditional procedural programming, this is not a problem — you simply use a variable — but the problem occurs as soon as we start creating functions during execution. To illustrate this problem, first give an example with Python. Here, make_fun() creates and returns a function called func_to_return , which is then used in the rest of the program:

 # Closures.py def make_fun(): #     : n = 0 def func_to_return(arg): nonlocal n #  'nonlocal' n += arg : #     'n'    print(n, arg, end=": ") arg += 1 n += arg return n return func_to_return x = make_fun() y = make_fun() for i in range(5): print(x(i)) print("=" * 10) for i in range(10, 15): print(y(i)) """ : 0 0: 1 1 1: 3 3 2: 6 6 3: 10 10 4: 15 ========== 0 10: 11 11 11: 23 23 12: 36 36 13: 50 50 14: 65 """ 


Note that func_to_return works with two fields that are not in its scope: n and arg (depending on the specific case, arg can be a copy or refer to anything outside the scope). A nonlocal declaration is mandatory because of the Python device itself: if you are just starting to work with a variable, then this variable is assumed to be local. Here, the compiler (yes, there is a compiler in Python and yes, it does some — admittedly very limited — a check of static types) sees that n += arg uses n, which was not initialized in the scope of func_to_return , so a message is generated by mistake. But if we say that n is nonlocal , Python will guess that we use n , defined outside the scope of the function, and which has been initialized, so that everything is in order.

So, we encounter such a problem: if we simply return func_to_return , what will happen to n outside the scope of func_to_return ? As a rule, one would expect n to go out of scope and become unavailable, but if this happens, func_to_return will not work. To support the dynamic creation of functions, func_to_return should “close” around n and ensure that it “survives” until the function returns. Hence the term "closure".

To test make_fun() , we call it twice and save the result of the function in x and y. The fact that x and y give completely different results demonstrates that with each call to make_fun() , a completely independent function func_to_return with its own closed storage for n .

Lambda expressions in java 8

Consider the same Java example using lambda expressions:

 // AreLambdasClosures.java import java.util.function.*; public class AreLambdasClosures { public Function<Integer, Integer> make_fun() { //     : int n = 0; return arg -> { System.out.print(n + " " + arg + ": "); arg += 1; // n += arg; //     return n + arg; }; } public void try_it() { Function<Integer, Integer> x = make_fun(), y = make_fun(); for(int i = 0; i < 5; i++) System.out.println(x.apply(i)); for(int i = 10; i < 15; i++) System.out.println(y.apply(i)); } public static void main(String[] args) { new AreLambdasClosures().try_it(); } } /* Output: 0 0: 1 0 1: 2 0 2: 3 0 3: 4 0 4: 5 0 10: 11 0 11: 12 0 12: 13 0 13: 14 0 14: 15 */ 


An ambiguous thing: we can really turn to n, but as soon as we try to change n, problems begin. The error message is: local variables referenced from a lambda expression must be final or effectively final (local variables referenced from a lambda expression must be final or in fact final).

It turns out that lambda expressions in Java are closed only around values, but not around variables. Java requires that these values ​​be immutable, as if we declared them final. So they must be final regardless of whether you declared them that way or not. That is, "actually final." Therefore, in Java there are “closures with constraints”, and not “full-fledged” closures, which, nevertheless, are quite useful.

If we create objects that are not located in the heap, then we can change such objects, since the compiler only keeps track of the link itself. For example:

 // AreLambdasClosures2.java import java.util.function.*; class myInt { int i = 0; } public class AreLambdasClosures2 { public Consumer<Integer> make_fun2() { myInt n = new myInt(); return arg -> ni += arg; } } 


Everything compiles without problems, to make sure you can just put the final keyword in the definition of n . Of course, if we apply such a move with any competition, then there will be a problem with a variable shared state.

Lambda expressions - at least in part - allow you to achieve the desired goal: now you can create functions dynamically. If you go beyond the boundaries, you will receive an error message, but usually similar problems are solved. The output will not be as straightforward as in Python, but this is still Java. And the end result, not without some restrictions (let's admit, any result in Java is not without some restrictions) is not so bad.

I wondered why these structures were called “lambdas” and not just “closures” - after all, by all indications, these are pure closures. I was told that “closure” is an unfortunate and overloaded term. When someone says “real closure”, they often mean such “closures” that they got in the first mastered programming language, where there were entities called “closures”.

I do not see here the dispute "OOP against OP", however, and was not going to arrange it. Moreover, I do not even see "against" here. OOP is well suited for data abstraction (and even if Java forces you to work with objects, this does not mean that any problem is solved with the help of objects), and the FP is for abstracting behaviors. Both paradigms are useful, and, in my opinion, all the more useful if they are mixed in both Python and Java 8. I recently had the opportunity to work with Pandoc, a converter written in the purely functional Haskell language, and I have the most positive impressions. So, purely functional languages ​​also deserve a place under the sun.

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


All Articles