I chose a lazy person to do a hard job.
Because a lazy person will find an easy way to do it. ― Bill Gates
Java is said to be a strict language, which means that expressions are evaluated eagerly, as soon as they are declared, by opposition with lazy languages in which expression are evaluated only when their value is needed. The difference is important because some expression values may never be needed, depending on some conditions.
Lazy evaluation saves computing resources by avoiding unneeded computations. Laziness makes possible to manipulate data that would be impossible to use in computations, such as infinite lists, or finite data that would overflow the available memory, such as huge files. It's then important to be able to use lazy evaluation even in languages that are strict by nature, like Java. In such languages, we need to learn how to implement lazy types, which is what we will do in this article.
In a previous article, I explained what laziness means. I talked about where laziness may be implemented (at language level or at type level), and when evaluation would eventually occur. We saw that call by name causes evaluation to occur each time a value is needed, whereas call by need consists of evaluating the value on the first need and storing it in case it would be needed again later. We also saw that laziness allows saving processing resources in case a value would eventually not be needed. But there is more to it.
[author_more]
Abstracting Computational Contexts
Laziness is not only a pattern to save resources. It is a computational context that can be applied to any expression. In this sense, it is not to be opposed to strictness (the fact that evaluation is performed as soon as an expression is defined). It is to be opposed to expressions out of context. Expressions can be used out of context or in context. Of course, one could argue that all expressions are defined in the context of a program. So let's say that some expressions may be defined in an additional context layer. And since all expressions are defined in the context of a program (as far as we are concerned), we will forget about this "top" context, and consider only the additional context layer.
Laziness is a specific context. When an expression is put in this context, it simply means that it might not be already evaluated. And furthermore, putting it into this context prevents evaluation, until we need it to occur. Note that we might however put in laziness context an expression that is already evaluated. This would of course change nothing regarding evaluation, but it could be useful to combine expressions, as we will see soon.
There are lots of other possible computational contexts. We could manipulate expressions which take a long time to evaluate. These expressions could be put in a different context in which evaluation would start immediately, but allowing us to manipulate them before evaluation completes. Or we could put expressions in a specific context allowing us to deal transparently with error conditions. In such a context, an expression would be evaluated either to its result or to an error. But what about an evaluation that would take a long time and might produce an error? Well, we could define a specific context for this, but we might better compose the two former contexts. Composing contexts of different types is an interesting problem. But for the moment, we will only consider composing several instances of the same context, meaning composing laziness. But before looking at this, we must first implement type laziness.
A Simple Approach to Type Laziness
Programmers have always known how to implement a minimal amount of laziness where they needed it. The simplest way to implement a lazy property is probably the following:
private String message;
public String getMessage() {
if (message == null) {
message = constructMessage();
}
return message;
}
In this example, the message property is lazily evaluated the first time we call its getter. The constructMessage method is "lazily" called, but this is not because of the method call itself, but because it happens in an if conditional structure, which is lazy. Unfortunately, we can't use this technique for method arguments because as soon as we will use the message property as a method argument, it will be evaluated, even if we do not use the value:
public String message;
public String getMessage() {
if (message == null) {
message = constructMessage();
}
return message;
}
private String constructMessage() {
System.out.println("Evaluating message");
return "Message";
}
public void doNothingWith(String string) {
// do nothing
}
public void testLaziness() {
doNothingWith(getMessage());
}
This example will print Evaluating message on the console, although we never use the resulting value. Can we do better?
Using an Existing Type
Yes, we can. We always could, but it is much easier now that we have lambdas and method references. Before Java 8, we could have written:
public void doNothingWith(Supplier<String> string) {
// do nothing
}
public void testLaziness() {
doNothingWith((new Supplier<String>() {
@Override
public String get() {
return getMessage();
}
}));
}
Of course, since the Supplier interface did not exist, we would have had to create it, which is reasonably easy:
public interface Supplier<T> {
T get();
}
With Java 8, we can use the standard java.util.function.Supplier interface and a lambda:
public void testLaziness() {
doNothingWith(() -> getMessage());
}
Or better, we can replace the lambda with a method reference:
public void testLaziness() {
doNothingWith(this::getMessage);
}
Note that these examples are not equivalent. Using an anonymous class will cause the creation of an instance of this class. On contrary, using a lambda will not cause the creation of an object, but only of a method. And if using a method reference, no method will even be created, since the referenced method will simply be called. This has some importance in terms of efficiency, but not only. The main consequence, from the programmer's point of view, is that the this reference will refer to the anonymous class in the first case, and to the enclosing class in case of a lambda or a method reference.
Composing Lazy Types
The laziness we gain by using Supplier is useful, but we can make it much more powerful. Imagine we have two lazy strings that we want to concatenate:
private String constructMessage(
Supplier<String> greetings, Supplier<String> name) {
return greetings.get() + name.get();
}
What is the benefit of laziness if we are forced to evaluate expressions to compose them? If we know for sure that we will need the result of the composition, it makes sense to evaluate expressions before composing them. But we might want to compose two lazy expressions and get a lazy result. This means that we would want to compose to values in context without having to take them out of context. For this, we just have to wrap the composition in a new instance of the lazy context, which means a Supplier:
private Supplier<String> constructMessage(
Supplier<String> greetings, Supplier<String> name) {
return () -> greetings.get() + name.get();
}
Of course, we have to change the type of the returned value. Instead of returning the result of composing the values, we are returning a program that, when executed (meaning when Supplier.get() is called), will evaluate both strings and compose them to create the expected result. This is what laziness is: small programs wrapping expressions that will be evaluated when these programs are executed.
Designing an Advanced Lazy Type
Continue reading %Lazy Computations in Java with a Lazy Type%
by Pierre-Yves Saumont via SitePoint
No comments:
Post a Comment