3.5. Composition
A single-argument function transforms one value into another. If you have two such transformations, then doing one after the other is also a transformation. Consider image manipulation: Let’s first brighten an image, then turn it to grayscale (see Figure 3-2).
Figure 3-2 First, the image is brightened, and then grayscale is applied.
That is easy to do with our transform method:
Image image = new Image("eiffel-tower.jpg"); Image image2 = transform(image, Color::brighter); Image finalImage = transform(image2, Color::grayscale);
But this is not very efficient. We need to make an intermediate image. For large images, that requires a considerable amount of storage. If we could compose the image operations and then apply the composite operation to each pixel, that would be better.
In this case, the image operations are instances of UnaryOperator<Color>. That type has a method compose that, for rather depressing reasons that are explored in Exercise 10, is not useful for us. But it is easy to roll our own:
public static <T> UnaryOperator<T> compose(UnaryOperator<T> op1, UnaryOperator<T> op2) { return t -> op2.apply(op1.apply(t)); }
Now we can call
Image finalImage = transform(image, compose(Color::brighter, Color::grayscale));
That is much better. Now the composed transformation is directly applied to each pixel, and there is no need for an intermediate image.
Generally, when you build a library where users can carry out one effect after another, it is a good idea to give library users the ability to compose these effects. See Exercise 11 for another example.