Javaによる関数型プログラミング ―Java 8ラムダ式とStream
目次
- 目次
- はじめに
- 複数の条件を元にソート
- 同じデータでグルーピングするgroupby
- 一つのリストから、複数の要素を作り、一つのリストにまとめる (flatmap)
- 複数の関数を繋げるチェーンラムダ
- 参考資料
- MyEnigma Supporters
はじめに
ちゃんとした関数型プログラミングは少し敷居が高いですが、
高階関数をベースとしたシンプルなものは、使えると便利なので、
様々な言語で実装したものを、まとめておきます。
複数の条件を元にソート
ある構造体(クラス)のリストを複数の条件でソートしたい場合は、
各言語で下記のように、高階関数を使って、簡単にソートできます。
Python
Pythonの場合は、sorted関数に与えるラムダで、
ソートに使いたい条件の値を複数タプルで返すと、
タプルの先頭を優先してソートしてくれます。
from dataclasses import dataclass @dataclass class Person: name: str = "" age: int = 0 person_list = [Person("John", 20), Person("Mary", 23), Person("Ann", 23), Person("Peter", 18)] print(f"{person_list=}") # person_list=[Person(name='John', age=20), Person(name='Mary', age=23), Person(name='Ann', age=23), Person(name='Peter', age=18)] sorted_list = sorted(person_list, key=lambda p: (p.age, p.name)) print(f"{sorted_list=}") # sorted_list=[Person(name='Peter', age=18), Person(name='John', age=20), Person(name='Ann', age=23), Person(name='Mary', age=23)]
Java
Javaでは、ComparatorのcomparingとthenComparingをつなげて、
sortedを呼ぶと複数条件でソートできます。
package fanctional_programing; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; import static java.util.Comparator.comparing; public class MultiConditionSort { static class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "{name='" + name + '\'' + ", age=" + age + '}'; } } public static void main(String[] args) { List<Person> personList = new ArrayList<>(Arrays.asList( new Person("John", 20), new Person("Mary", 23), new Person("Ann", 23), new Person("Peter", 18) )); System.out.println(personList); // [{name='John', age=20}, {name='Mary', age=23}, {name='Ann', age=23}, {name='Peter', age=18}] Function<Person, Integer> byAge = person -> person.age; Function<Person, String> byName = person -> person.name; List<Person> sortedList = personList.stream() .sorted(comparing(byAge).thenComparing(byName)) .collect(Collectors.toList()); System.out.println(sortedList); // [{name='Peter', age=18}, {name='John', age=20}, {name='Ann', age=23}, {name='Mary', age=23}] } }
Julia
JuliaもPythonと同様に、sort関数のby引数にわたすラムダに、
ソートに使いたい条件の値を複数タプルで返すと、
タプルの先頭を優先してソートしてくれます。
struct Person name::String age::Int64 end person_list = [Person("John", 20), Person("Mary", 23), Person("Ann", 23), Person("Peter", 18)] @show person_list # person_list = Person[Person("John", 20), Person("Mary", 23), Person("Ann", 23), Person("Peter", 18)] sorted_list = sort(person_list, by = p -> (p.age, p.name) ) @show sorted_list # sorted_list = Person[Person("Peter", 18), Person("John", 20), Person("Ann", 23), Person("Mary", 23)]
同じデータでグルーピングするgroupby
ある構造体(クラス)のリストから、構造体のあるデータを元にグルーピングする
いわゆるgroup byが下記のように実装できます。
Python
Pythonでは、標準ライブラリであるItertollsのgroupbyを使うと便利です。
注意点として、groupbyの返り値はイテレータなので、dict内表記を使って、
dictに変換できます。
from dataclasses import dataclass from itertools import groupby @dataclass class Person: name: str = "" age: int = 0 person_list = [Person("John", 20), Person("Mary", 23), Person("Ann", 23), Person("Peter", 18)] print(f"{person_list=}") # person_list=[Person(name='John', age=20), Person(name='Mary', age=23), Person(name='Ann', age=23), Person(name='Peter', age=18)] groupby_map = {k: list(v) for k, v in groupby(person_list, lambda p: p.age)} print(f"{groupby_map=}") # groupby_map={20: [Person(name='John', age=20)], 23: [Person(name='Mary', age=23), Person(name='Ann', age=23)], 18: [Person(name='Peter', age=18)]}
Java
Javaの場合は、Collector.groupingByで、
条件をもとに、hashmapでデータをグルーピングできます。
package fanctional_programing; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class GroupBy { static class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "{name='" + name + '\'' + ", age=" + age + '}'; } } public static void main(String[] args) { List<Person> personList = new ArrayList<>(Arrays.asList( new Person("John", 20), new Person("Mary", 23), new Person("Ann", 23), new Person("Peter", 18) )); System.out.println(personList); // [{name='John', age=20}, {name='Mary', age=23}, {name='Ann', age=23}, {name='Peter', age=18}] Map<Integer, List<Person>> groupBy = personList.stream() .collect(Collectors.groupingBy(person -> person.age)); System.out.println(groupBy); // {18=[{name='Peter', age=18}], 20=[{name='John', age=20}], 23=[{name='Mary', age=23}, {name='Ann', age=23}]} } }
Julia
残念ながらJuliaは標準ライブラリとして
groupbyは対応してないので、
Juliaの本体には、まだgroupby(dataframeのやつではなく)って、無いのか。:Add groupby to the standard lib · Issue #32331 · JuliaLang/julia https://t.co/n0SatSpYoz
— Atsushi Sakai (@Atsushi_twi) 2023年1月8日
下記のように、Groupbyっぽい関数を定義すれば、 同じような処理が可能です。
struct Person name::String age::Int64 end function groupby(list::Array{T}, f::Function) where T KeyType = Core.Compiler.return_type(f, Tuple{T}) groups = Dict{KeyType, Array{T}}() for v in list push!(get!(groups, f(v), Array{T}[]), v) end return groups end person_list = [Person("John", 20), Person("Mary", 23), Person("Ann", 23), Person("Peter", 18)] @show person_list # person_list = Person[Person("John", 20), Person("Mary", 23), Person("Ann", 23), Person("Peter", 18)] groupby_map = groupby(person_list, p -> p.age) @show groupby_map # groupby_map = Dict{Int64, Array{Person}}(20 => [Person("John", 20)], 18 => [Person("Peter", 18)], 23 => [Person("Mary", 23), Person("Ann", 23)])
ちなみにこの関数の作成はは、Twitter上のJuliaに詳しい方にサポートしてもらいました(^^)。
書いてみました。Undocumentedな関数ですがCore.Compiler.return_type()というのがあったのでそれ使ったらたぶん希望の動きになりました。
— あんちもん2 (@antimon2) 2023年1月9日
→ https://t.co/s6e5ItW3Tt
(https://t.co/o8us0sYqAM) pic.twitter.com/hNTlimI9Yl
一つのリストから、複数の要素を作り、一つのリストにまとめる (flatmap)
一つのリスト情報から、複数の情報を作って、
それを一つのリストとして並べる処理をflatmapなどといいます。
Python
Pythonの場合は、リスト内包表記の前のfor の要素を
後ろのforで使うことでflatmapができます。
from dataclasses import dataclass @dataclass class Person: name: str = "" age: int = 0 person_list = [Person("John", 20), Person("Mary", 23), Person("Ann", 23), Person("Peter", 18)] print(f"{person_list=}") # person_list=[Person(name='John', age=20), Person(name='Mary', age=23), Person(name='Ann', age=23), Person(name='Peter', age=18)] flatmap_list = [p2 for p in person_list for p2 in (p.age, p.name)] print(f"{flatmap_list=}") # flatmap_list=[20, 'John', 23, 'Mary', 23, 'Ann', 18, 'Peter']
Java
Javaの場合は、公式APIのflatMapが使えます。
package fanctional_programing; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; public class FlatMap { static class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "{name='" + name + '\'' + ", age=" + age + '}'; } } public static void main(String[] args) { List<Person> personList = new ArrayList<>(Arrays.asList( new Person("John", 20), new Person("Mary", 23), new Person("Ann", 23), new Person("Peter", 18) )); System.out.println(personList); // [{name='John', age=20}, {name='Mary', age=23}, {name='Ann', age=23}, {name='Peter', age=18}] List<String> flatMapList = personList.stream() .flatMap(x -> Stream.of(String.valueOf(x.age), x.name)) .collect(Collectors.toList()); System.out.println(flatMapList); // [20, John, 23, Mary, 23, Ann, 18, Peter] } }
Julia
JuliaはPythonと同じようにできます。
struct Person name::String age::Int64 end person_list = [Person("John", 20), Person("Mary", 23), Person("Ann", 23), Person("Peter", 18)] @show person_list # person_list = Person[Person("John", 20), Person("Mary", 23), Person("Ann", 23), Person("Peter", 18)] flatmap_list = [p2 for p in person_list for p2 in (p.age, p.name)] @show flatmap_list # flatmap_list = Any[20, "John", 23, "Mary", 23, "Ann", 18, "Peter"]
複数の関数を繋げるチェーンラムダ
複数の関数やラムダをつないで、
一つのデータを複数の処理をするのが、チェーンラムダです。
Java
Javaの場合は、composeとandThenメソッドを使うことで、
ラムダを連結することができます。
import java.util.Arrays; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; public class ChainLambda { static class Person { String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "{name='" + name + '\'' + ", age=" + age + '}'; } } public static void main(String[] args) { List<Person> personList = Arrays.asList( new Person("John", 20), new Person("Mary", 23), new Person("Ann", 23), new Person("Peter", 18) ); System.out.println(personList); // [{name='John', age=20}, {name='Mary', age=23}, {name='Ann', age=23}, {name='Peter', age=18}] Function<Person, String> get_name = (x) -> x.name; Function<String, String> make_uppercase = String::toUpperCase; List<String> flatMapList = personList.stream() .map(get_name.andThen(make_uppercase)) .collect(Collectors.toList()); System.out.println(flatMapList); // [JOHN, MARY, ANN, PETER] } }
Julia
Juliaの場合は、chained function operator (|>)を使うと、
結構綺麗に書けます。
struct person name::String age::Int64 end person_list = [person("john", 20), person("mary", 23), person("ann", 23), person("peter", 18)] @show person_list # person_list = person[person("john", 20), person("mary", 23), person("ann", 23), person("peter", 18)] chainedlambda = person_list .|> (p -> p.name) .|> uppercase @show chainedlambda # chainedlambda = ["JOHN", "MARY", "ANN", "PETER"]
参考資料
Javaによる関数型プログラミング ―Java 8ラムダ式とStream
MyEnigma Supporters
もしこの記事が参考になり、
ブログをサポートしたいと思われた方は、
こちらからよろしくお願いします。