List processing understood by pictures --java8 stream / javaslang- Extra article!
It was too long as usual, so I separated it.
Let's go roughly!
There are three definitions of
reduce
in java8 stream,
I will introduce the other two.
The first is this.
Optional<T> reduce(BinaryOperator<T> accumulator);
Optional<T> reduce( ((T, T) -> T) f); //Simplification
T reduce(T t, ((T, T) -> T) f); //Repost of the first reduce
The difference from before is that there is no T t
and the return is wrapped in ʻOptional`.
You can see why this happens by looking at the picture.
Those who have the first T t
Those who do not have T t
The difference between specifying the initial value by yourself and using the first one as the initial value is the difference in the presence or absence of T t
.
So, you can see why the return is ʻOptional` by considering the case of an empty list.
If the list is empty, T t
can be the final result
Nothing happens if there is no T t
and the list is empty
This is because if there is no initial value and the list is empty, a return cannot be prepared.
I will introduce the remaining one.
The second is this.
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
U reduce(U u, ((U, T) -> U) f, ((U, U) -> U) g); //Simplification
U reduce(U u, ((U, T) -> U) f); //For the time being, forget the third argument
T reduce(T t, ((T, T) -> T) f); //Repost of the first reduce
If you forget the mysterious third argument, it's very similar to the first reduce
.
This means that even if the initial value is different from the list type, it can be folded, and in other languages, this form is the standard.
So, as for the last 3 arguments, java8 stream considers parallel execution, so it requires a method to merge the results of split execution. To be honest, I want you to hide this.
(This is really an "image" because I haven't read the internal processing properly)There may be some resistance to the 3 argument, but if you think that the 3 argument is noise, it is the same as other languages, so it is good to learn it without avoiding it.
If you want to know the sum of [10, 30, 45, 15]
, you should use sum
. That's right.
reduce
is only valuable if the initial types are different. If you can use a different type for the initial value, you can actually do it.
For example, you can quickly write the process of checking if the list of (
and )
is closed correctly with reduce
.
resolve(asList('(', '(', ')', ')')) // true
resolve(asList('(', '(', ')')) // false
resolve(asList('(', ')', ')', '(')) // false
private static boolean resolve(List<Character> cs) {
return cs.stream()
.reduce(
Optional.of(0),
(acc, c) -> c == '('
? acc.map(n -> n + 1)
: acc.map(n -> n - 1).filter(n -> n >= 0),
(acc1, acc2) -> acc1
.flatMap(v1 -> acc2.map(v2 -> v1 + v2))
).equals(Optional.of(0));
}
Set the initial value to ʻOptional (0), and while convolving, set
.map (n + 1) to ʻOptional (n)
for (
, and .map to
.map (n) for
) . Set to (n -1)
. However, if it falls below ʻOptional (0), it will be ʻempty
.
Once it becomes ʻempty, even if you do ʻempty.map (n + 1)
, it will never return to ʻOptional (0)`.
If it is ʻOptional (0)at the end after folding, it means that the number of
(and
)is the same, and there was never too much
)`.
The third argument that merges two ʻOptionals can be added by looking inside if both are ʻOptional (n)
. If both ~ ~, that is flatMap
. It feels good if this comes out soon.
U reduce(U u, ((U, T) -> U) f, ((U, U) -> U) g); //Repost
Let's learn while staring at the pattern.
Since reduce has to be careful about the order and direction of calculation, I will give a quick example.
The two have the same result.
Stream.of(1, 2, 3).reduce((acc, n) -> acc + n).orElse(0) // 6
Stream.of(1, 2, 3).reduce(0, (acc, n) -> acc + n) // 6
But the two results are different.
Stream.of("1", "ok", "15:10").reduce((acc, s) -> acc + " | " + s).orElse("") // 1 | ok | 15:10
Stream.of("1", "ok", "15:10").reduce("", (acc, s) -> acc + " | " + s) // | 1 | ok | 15:10
You can understand the difference by drawing a picture yourself.
Good to remember.
Now that we've touched on reduce
, let's take a look at javaslang's reduce
.
List.of("1", "ok", "15:10").reduceRightOption((s, acc) -> acc + " | " + s).getOrElse(""); // 15:10 | ok | 1
Reduce
, which folds from the right, is also available depending on the language and library. (Note that this is not (acc, s)->
but (s, acc)->
)
Of course, even if you use this, the result will often be different from the case from the left, so be careful.
This is the end of reduce
!
Introducing takeWhile
, which is useful to remember by list operation. This is introduced using javaslang.
(I didn't know that java8 stream doesn't have takeWhile
...)
Let's put the label: millisec
line in the first subject, in order from the earliest, to less than 30.
lines
.filter(line -> line.contains(":"))
.map(line -> Integer.valueOf(line.split(":")[1]))
.sorted()
.takeWhile(n -> n < 30); // [10, 15]
takeWhile
is like" picking up from the beginning only while certain conditions are met. "
I use it a lot with dropWhile
, so it's good to remember this too.
Lastly, I will introduce another one, zip
, which I use a lot unexpectedly. This is introduced using javaslang.
It's like pairing the same part of two lists. It may be easier to understand if you think it feels like a zipper.
List.of(1, 2, 3)
.zip(List.of('a', 'b', 'c')) // [(1, a), (2, b), (3, c)]
(`Zip` cannot be used unless there is a type that can pair different types such as` Tuple `.)
Other than when you want to pair ʻA and
B, for example, when you want to know the size of each gap in the list of ʻInteger
, try zip
by shifting the same list by one. There is a way.
List<Integer> times = List.of(10, 15, 35, 60); //this[15 - 10, 35 - 15, 60 - 35]Want to
times
.zip(times.subSequence(1))
.map(t -> t._2 - t._1); // [5, 20, 25]
Do you use reduce
when you think about processing from the beginning? You might think, but if you make a picture, it's completely different.
(Reduce
does not calculate with the neighbor, but calculates the sum (or) up to that point one by one, so the gap calculation cannot be straightforward.)
If you do filter and map
well on a log file, zip
, take the processing time difference for each line, and try reverse sort and takeWhile
, the lines that take 500 ms or more are displayed in order of slowness. You can do it.
There are definitely talks such as "I don't know the merits" and "Isn't it okay with for?", But of course there are merits that are worth the learning cost.
There are some, but I'll briefly list three.
For example, there is such a function of String-> Integer
.
private static Integer toIntAndTwice(String s) {
return Integer.valueOf(s) * 2;
}
The code that applies this function to "when there are multiple String
s "," when there is at most one String
", and "when there is a String
that may be malformed "is Become.
List
example
List<Integer> result = List.empty();
List<String> org = ...; // List.of(1, 2, 3) or empty
for (String x : org) {
result.append(toIntAndTwice(x));
}
return result;
ʻOption` example
Option<Integer> result;
Option<String> org = ...; // Option.of(1) or empty
if (org.isEmpty()) {
result = Option.none();
} else {
result = Option.of(toIntAndTwice(org.get()));
}
return result;
try
example
Integer result;
String org = ...; // "1" or "x"
try {
result = toIntAndTwice(org);
} catch (Throwable t) {
result = null;
}
return result;
You have to write a completely different code to apply toIntAndTwice
to String
in a particular situation.
If you write this with map
, it will be like this.
List
example
List<String> org = ...;
List<Integer> mapped = org.map(JSMain::toIntAndTwice);
return mapped;
ʻOption` example
Option<String> org = ...;
Option<Integer> mapped = org.map(JSMain::toIntAndTwice);
return mapped;
Try
example
Try<String> org = ...;
Try<Integer> mapped = org.map(JSMain::toIntAndTwice);
return mapped;
It looks the same!
This is because "the rules of List
and ʻOption in a specific situation" and "what you actually want to do (
toIntAndTwice`) "are separated, and the former is followed by the language.
By the way, if the code is similar so far, I feel that it can be made more common, right?
Since List
and ʻOption of javaslang inherit from
Value`, you can also do this.
If you define such Value <T>-> Value <R>
,
private static Value<Integer> mapAnyType(Value<String> org) {
return org.map(JSMain::toIntAndTwice);
}
It works whether the argument is List
or ʻOption`!
List
example
Value<Integer> m1 = mapAnyType(List.of("1", "2", "3")); // List(2, 4, 6)
ʻOption` example
Value<Integer> m2 = mapAnyType(Option.none()); // None
Try
example
Value<Integer> m3 = mapAnyType(Try.of(() -> "x")); // Failure(java.lang.NumberFormatException: For input string: "x")
In this case, the same code can handle different situations, so for example, prepare a cancellation process for paid options
, and have a" function to cancel all at once (List
)" and a "function to cancel if you have one (ʻOption". ")" And "The function to cancel because you should have it ( Try
) "can be realized at once.
I think the biggest merit is the separation of "situation" and "processing".
Legacy code that everyone loves. This is common.
//Initialization
result = 0;
flag = false;
for (i ...) {
result = ...;
//Is ~ ~ finished?
if (i < ...) {
result = ...;
flag = true;
}
//If it is ~~, it ends
if (flag) {
return result;
}
//Then loop with ~ ~
for (j ...) {
result = ...;
//If it is ~~, it ends
if (j < ...) {
return result;
}
}
//Initialization
flag = false;
}
//return
return result;
The return result;
in this code is the same as the text, but the contents are completely different. (Maybe, I don't even know.)
Since there is a context in the lines, you can not copy and paste only that line, and it seems that you are making blocks with comments, but in reality this is only one huge block.
If this looks like the code above, all three lines of the argument work independently, and there are no variables in the method scope that shouldn't be return
.
(Since there is one ;
, this code is one line. Therefore, there can be no gap state.)
return cs.stream()
.reduce(
Optional.of(0),
(acc, c) -> c == '(' ? acc.map(n -> n + 1) : acc.map(n -> n - 1).filter(n -> n >= 0),
(acc1, acc2) -> acc1.flatMap(v1 -> acc2.map(v2 -> v1 + v2))
).equals(Optional.of(0));
I think that this is overwhelmingly more reusable and of higher quality. (Of course, if you do it with shared code, I think it's better to be a little more careful. The second argument should be named properly and the test code should be written lightly. I think the 3rd edition also said that.)
I will touch on the details next, but the more you know the idea itself, the better it will be at first glance unless it is a special language.
If you learn java8 stream and go to ruby, you will be able to operate the list immediately, and even if you are new to java8, if you are experienced with python, you will be able to stream.
As mentioned in the last-minute benefits, most languages have map, filter, reduce
.
When you have to maintain a language you don't know in a hurry at work, or when you want to make some changes to the tools you picked up, all you need to know is the words and the processing image. So, at the end, I will end with a summary of how to do the same processing in the language you often hear.
(The part with (*)
feels like you can do something similar if you use it well)
lang | map | filter | reduce(zero / left) reduce(head / left) reduce(zero / right) reduce(head / right) |
take while | zip |
---|---|---|---|---|---|
java | map | filter | reduce reduce - - |
- | - |
groovy | collect | findAll | inject inject - - |
takeWhile | transpose (*) |
scala | map | filter | reduceLeft / reduceLeftOption foldLeft reduceRight / reduceRightOption foldRight |
takeWhile | zip |
python | map | filter | reduce - - - |
itertools.takewhile | zip |
php | array_map | array_filter | array_reduce array_reduce - - |
- | array_map (*) |
ruby | map / collect | select | reduce / inject reduce / inject - - |
take_while | zip |
js | map | filter | reduce reduce reduceRight reduceRight |
- | - |
haskell | map | filter | foldl foldl1 foldr foldr1 |
takeWhile | zip |
If you remember the words around map / collect
, filter
, reduce / fold / inject
, most languages will work.
map vs collect
and reduce vs inject
may be interesting to look into.
If anyone has read this far, thank you.
It was good that the article was in time on the day I declared it. That's it.
Recommended Posts