Articles to learn more about Stream API

Introduction

This is an article on December 20th of Works Human Intelligence Advent Calendar 2020 (https://qiita.com/advent-calendar/2020/whi), which embodies the Advent Calendar [Develop fun!] Of Works Human Intelligence Co., Ltd. If you don't mind, please take a look at other articles.

Assumed reader

What is Stream API?

The Stream API is a mechanism added from Java 8 that conveniently handles iterative processing for collections. The Oracle documentation explains:

** Description of package java.util.stream ** Map to collection-A class that supports functional operations on a stream of elements, such as reduce transforms. https://docs.oracle.com/javase/jp/8/docs/api/java/util/stream/package-summary.html

The Stream API allows you to write complex iterations in several steps, allowing you to write readable and maintainable code. For example, compare the simple example of "summing a sequence of numbers from 1 to 100".

int sum = 0;
for (int i = 1; i <= 100; ++i) {
    sum += i;
}
int sum = IntStream.rangeClosed(1, 100).sum();

The important thing is not that the code is in one line, but that the process is divided into stages. Coloring green as a sequence, yellow as a result, and so on:

スクリーンショット 2020-11-21 135607.png

As you can see, in the good old for, yellow is scattered in two places, but in Stream, it is gathered in one place. This means that the process is organized step by step.

Normally, the process of processing the generated collection is also included, so there will be more code between green and yellow. The more complex the content of the iteration, the more meaningful it is to separate collection generation, processing, and result generation.

In this way, the Stream API is a mechanism that incorporates the good points of functional styles into Java and provides iterative processing that is easy to read and write.

Also, because there are mechanisms similar to the Stream API in different languages, what you've learned here can be applied and used in different languages ​​in the same way.

Reference: Why do we stubbornly avoid for

Stream API splits the power of for statements

A for statement is, so to speak, a "god of iteration" that can express any iteration. And the Stream API is the result of refactoring this iterative god.

The following figure shows the process by which the repetitive force is divided.

goto (Unconditional jump)
+-> if (Conditional branch)
+-> try-catch-finally (Exception handling)
+-> for (Iterative processing= while,Recursive function)
    +-> unfold (Deployment)
        +-> Stream.iterate
        +-> Stream.generate
        +-> Stream.of
        +-> Stream.empty
        +-> IntStream.rangeClosed
        ...
    +-> Stream.reduce (Convolution)
        +-> IntStream.sum
        +-> Stream.max
        +-> Stream.allMatch
        +-> Stream.count
        +-> Stream.flatMap
        +-> Stream.map
            +-> Stream.peek
        +-> Stream.filter
        ...

Just as you can use goto for conditional bifurcation and iterative processing, you can use for to cover all of the Stream API. On the flip side, the Stream API is that much smaller iteration.

Tips: for and recursive functions have the same expressiveness

In addition to iterating with for (while, do-while), you can also iterate using recursive functions.

Iterative syntax and recursive functions look completely different, but they actually have exactly the same functionality. In other words, everything that can be written with for can be written with a recursive function, and vice versa.

However, for and recursive functions have different properties, and in practice it is necessary to use them properly.

Strongest function in Stream API

In the previous chapter, we explained that the Stream API is a for for each function. However, there are some relatively strong functions within this Stream API as well.

First of all, iterative processing can be roughly divided into two. The process of creating a collection such as a sequence (expansion, unfold) and the process of aggregating a collection into some value (convolution, fold ≒ reduce).

In other words, this unfold and fold are the pillars of the Stream API's functional division [^ 1]. Stream API functions basically belong to either of these.

[^ 1]: It is different from the distinction between intermediate operation and terminal operation.

reduce

The reduce in the Stream API is like a" god of convolution ", and you can use them to perform most convolution operations.

The range of convolution operations is wide, from the basics provided by the Stream API to such things! ?? You can even implement functions that you think are reduce.

intstream.sum();     // IntStream<Integer>Fold into an Int
list.size();         // List<T>Fold into an Int
list.flatMap(f);     // List<T>List<U>Fold in
insertionSort(list); // List<Integer>List<Integer>Fold in

flatMap

flatMap is a relatively strong function because it can implement map and filter in combination with singleton.

In addition to this, flatMap has the property of being able to perform multiple loops. In most cases, you will want to use flatMap in practice.

var list = List.of(1, 2, 3);
var result = list.stream().flatMap(x -> {
        return list.stream().map(y -> {
            return x * y;
        });
//map flatMap+Example of writing in singleton
//      return list.stream().flatMap(y -> {
//          return Collections.singletonList(x * y);
//      });
    })
    .collect(Collectors.toList());

This characteristic of flatMap is noticed in functional languages ​​such as Haskell, and the interface with only flatMap and singleton is called ** monad ** and is cherished. Writing Java-like pseudo code looks like this.

interface Monad<T> { 
    <U> Monad<U> flatMap(Function<T, Monad<U>> f);
    static Monad<T> singleton(T a);
}

By the way, isn't this interface similar to the JavaScript Promise?

Looking up at the sky, closing my eyes, thinking of a JavaScript Promise, turning to the monitor and opening a thin eye, I feel that flatMap looks like then and Monad.singleton looks like Promise.resolve. Would you like to ...?

Promise …… You …… Monad ……?

Promise.resolve(value); //Creating a promise from the value
p.then(value => {       //Passing a function that takes a value and returns a Promise
    console.log(value);
    return Promise.resolve(value);
});

Tips: Unfold

Unfolding in programming is the operation of creating a recursive structure from a single value, as opposed to convolution. Unlike convolution (reduce), the Stream API doesn't have a function called smashing, but if it did, it would look like this:

public static <T, U> Stream<U> iterate(
        T seed,                                    //seed
        Predicate<? super T> hasNext,              //Whether you can get the next seed and value from a seed
        UnaryOperator<T> next,                     //Get the next seed from a seed
        Function<? super T, ? extends U> mapper    //Get value from seed
);

A function that allows you to pass a mapper to the iterate overload added in Java 11. It is used as follows.

iterate(1,
        n -> true,
        n -> n + 1,
        n -> fizzbuzz(n))
    .limit(15)
    .forEach(System.out::println);

The final Stream is the collection of these values.

In the fizzbuzz example, * species * is a sequence of numbers greater than or equal to 1, and the value is a collection of integers converted by fizzbuzz. The above program produces the following output (supplementing the fizzbuzz function):

1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz

The range of expansion operations is wide, and roughly speaking, all functions with the following types are expanded.

<T, R> Stream<R> someunfold(T seed);

The simplest ones are Stream.of and Stream.empty. Stream.generate and IntStream.rangeClosed are also classified into this.

The point is that the operation of creating a Stream or collection is an expansion.

Stream API issues

Like you, I always want to replace the for statement with the Stream API.

However, in reality, for is still active, and unfortunately it is not the case when asked if we should replace everything with the Stream API.

Here, I'd like to take a side trip and point out some problems with Java's Stream API.

  1. Higher-order functions and Checked exceptions are incompatible
  2. Nested Stream makes it hard to read

Higher-order functions and Checked exceptions are incompatible

This is the hardest part of using the Stream API personally. I think there are many people who seemed to be able to write neatly using Stream, but were worried about rewriting for because they wanted to call a function with throws in the middle. However, it is not beautiful to wrap it in RuntimeException once and peel it off later.

There is a technique called Sneaky throw [^ 3] as a solution, but this is also a little hesitant to use. [^ 4]

[^ 3]: Reference: "Sneaky Throws" in Java https://www.baeldung.com/java-sneaky-throws [^ 4]: You can also use Lombok annotations if you are allowed to use them https://projectlombok.org/features/SneakyThrows

public static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
    throw (E) e;
}

Nested Stream makes it hard to read

Streams are nested when trying to run multiple loops in Stream. Originally, multiple loops are a complicated process, and nesting itself is the same for for, but the difficulty of reading when a Stream is nested is considerable.

var list = List.of(1, 2, 3);
var result = list.stream().flatMap(x -> {
        return list.stream().map(y -> {
            return x * y;
        });
    })
    .collect(Collectors.toList());

Do you know what you are doing at a glance? In this example, for may seem rather simple.

var list = List.of(1, 2, 3);
var result = new ArrayList<Integer>();
for (var x : list) {
    for (var y : list) {
        result.add(x * y);
    }
}

This issue isn't a Java or Stream API issue, it's just that it's hard to read if higher-order functions are nested. There is no solution to this in Java today, and it seems reasonable to use for as before when multiple loops are needed.

Tips: Solutions in functional languages

I mentioned earlier that flatMap is deeply linked to multiple loops, and that flatMap is important in functional languages. And problems that are difficult to read when higher-order functions are nested seem to occur regardless of paradigm.

So how does a functional language solve this problem?

In fact, functional languages ​​solve this problem using syntactic sugar. For example, in Scala's for statement, you can write the same multiple loop as above as below.

val list = 1 to 3
val result = for (x <- list; y <- list) yield {
    x * y
}

The nest is gone.

Scala for is desalted into calls to flatMap and map. That is, the Scala code above is equivalent to the code below.

val list = 1 to 3
val result = list.flatMap { x =>
    list.map { y =>
        x * y
    }
}

You can see that this is exactly the same as the code that makes full use of flatMap and map written in Java's Stream API. In this way you can (visually) eliminate nesting of higher-order functions.

in conclusion

In this article, I introduced the classification and properties of Stream API with the purpose of "Understanding Stream API better". I hope you can think that you have gained a new perspective and learning.

If you don't mind, please take a look at other articles. Works Human Intelligence Advent Calendar 2020 that embodies Develop fun!

Recommended Posts

Articles to learn more about Stream API
[Introduction to Java] About Stream API
[Java] Introduction to Stream API
[java8] To understand the Stream API
Learn more about gems and bundlers
Java 8 ~ Stream API ~ to start now
Anonymous class (aiming to introduce stream api)
I tried to summarize the Stream API
Stream API memo
Learn more about docker image and Dockerfile FROM
Chew about API
Java Stream API
Learn more about collections and members in routes.rb
[For beginners] About lambda expressions and Stream API
[Java] How to operate List using Stream API
Stream API basics
[For beginners] How to operate Stream API after Java 8
Stream API (Collectors class)
[Java] Stream API / map
Stream API map method
Java8 Stream API practice
About Apache Inference API
[Must-see for apprentice java engineer] How to use Stream API
Now is the time to get started with the Stream API
I was addicted to using Java's Stream API in Scala
Convert 2D array to csv format with Java 8 Stream API