# Javaによる関数型プログラミング ## 1.Hello Lambda式 価格が20以上の商品を値引きし、合計する例 * 仕様を忠実に実装しやすい * 遅延評価によって効率が上がる * 通信、DBへの問い合わせなどのコストがかかる処理は必要になってからやる * →6. * 並列化しやすい * レガシーなコードを並列化するのは、大規模な変更が必要 ![](https://i.imgur.com/6D1wUhn.png) * ポリシーの強制 ```java= Transaction tran = getFromTranFactory(); checkProgressAndCommitOrRollBackTran(); Update(); ``` ポリシーがメソッドにカプセル化される ```java= runWithinTran((Transaction tran) -> { update(); }) ``` * 関心の分離 * 今まではStrategyパターン * →4. * テスト環境の向上 * Unit TestのMockとしてラムダ式を使える * →4. 5. * 高階関数 * 関数に関数を渡す * 関数内で関数を生成する * 関数から関数を返す * シンタックスシュガー * 省略した文法 ラムダ ```java= price.stream().map(price -> price * 0.9); ``` Javaコンパイラによる合成 ```java= Double apply(Integer param) { int price = return Double.valueOf(price * 0.9); } ``` Stream ```java= Stream<R> map(Function<T, R) mapper) { = mapper.apply(...) } ``` ## 2.コレクションの使用 ### リストのイテレート * 古典的なループ ```java= for(int i =0; i < friends.size(); i++) { } ``` ↓ * 外部イテレータ * 並列化できない * Tell dont ask の原則に反する * Ask 状態を聞く→結果をみる→決定 * Tell オブジェクトに何をして欲しいか伝える ```java= for(String name : friends) { } ``` ↓ * 内部イテレータ 各要素に何を実行するかに集中できるようになった ```java= friends.forEach(new Consumer<String>() { public void accept(final String name) { } }); ``` ↓ * ラムダ ```java= friends.forEach(final String name) -> System.out.println(name)); ``` ↓ * ラムダ(型の推論) finalによる保護がないことに注意 ```java= friends.forEach((name) -> System.out.println(name)); ``` ↓ * さらに省略する ```java= friends.forEach(name -> System.out.println(name)); ``` ↓ * メソッド参照 * 何も削れないところまで削った ```java= friends.forEach(system.out.println) ``` ## リストの変換 * ラムダ ```java= friends.stream() .map(name -> name.toUpperCase()) .forEach(name -> print(name + " ")); ``` メソッド参照 ```java= friends.stream() .map(String::toUppercase) .forEach(name -> print(name + " ")); ``` * メソッド参照はいつ使うか * ラムダでただ引数を渡しているだけの場合 ## 要素の検索 * 今まで ```jav= List StrtsWithN = ; for (String name : friends) { if (name.startsWith("N")) { startsWithN.add(name); } } ``` * filterを使う ```java= friends.stream() .filter(name -> name.startsWith("N")) .collect(Collectors.toList()); ``` ## ラムダ式の再利用 * 関数を変数化して再利用する ```java= final Predicate<String> startsWithN = name -> name.startsWith("N"); final long countFriendsStartN = friends.stream() .filter(startsWithN) .count(); ``` ## 静的スコープとクロージャ ↑の例でBから始まる場合はどう再利用するか * 高階関数 * 引数で関数を受け取る関数 * 関数を返却する関数 * 静的スコープ * ラムダ式を定義しているスコープで変数letterを探す * よくわからん * コンテクストに別のコンテクストで使う値をキャッシュできる * クロージャともいう ```java= public static Predicate<String> checkIfStartsWith(final String letter) { return name -> name.startWith(letter); } final long countFriendsStartN = friends.stream() .filter(checkIfStartsWith("N")) .count(); ``` ## スコープを限定する ↑の例だとstaticメソッドが増えてクラスが汚れるため、変数化する。 ```java= final Function<String, Predicate<String>> startsWithLetter = (String letter) -> { Predicate<String> checkStarts = (String name) -> name.startsWith(letter); return checkStarts; } ``` さらにラムダで簡潔にする ```java= final Function<String, Predicate<String>> startsWithLetter = (String letter) -> (String name) -> name.startsWith(letter); ``` データ型の推測でさらに簡潔にする ```java= final Function<String, Predicate<String>> startsWithLetter = letter -> name -> name.startsWith(letter); ``` 呼び出しもと ```java= final long countFriendsStartN = friends.stream() .filter(startsWithLetter.apply("B")) .count(); ``` * FunctionとPredicateの違い * Function * boolean以外も返す * Predicate * 検査結果としてbooleanを返す ## 要素を一つ選択する * レガシーな記述 * NullPointerExceptionが発生しやすい * 命令型(break) * シンプルなことに対してコード量が多い ```java= public static void pickName(List names, String startingLetter) { String foundName = null; for(String name : names) { if(name.startsWith(startingLetter)) { foundName = name; break; } } if(foundName != null) } ``` * Optionalは結果が存在しない可能性がある場合に便利 * isPresentで値の存在 * getで取得 ```java= public static void pickName(List names, String startingLetter) { final Optional<String> foundName = names.stream() .filter(name -> name.startsWith(startingLetter)) .findFirst(); System.out.println(~, foundName.orElse("No name found"))); ``` ## コレクションを単一の値に集約 (reduce) * reduce * 2つの要素を比較し、その結果を次の要素の結果と比較する 最長の名前を取得する ```java= final Optional<String> aLongName = friends.stream() .reduce((name1, name2) -> name1.length() >= name2.length() ? name1 : name2); ``` デフォルトを指定する Steveより長い名前がなければSteve ```java= final Optional<String> aLongName = friends.stream() .reduce("Steve", (name1, name2) -> name1.length() >= name2.length() ? name1 : name2); ``` # 3.文字列、コンパレータ、フィルタ ## 文字列のイテレーション ```java= str.chars() .filter(Character::isDigit) .forEach(IterateString::printChar); ``` * メソッド参照の注意点 * インスタンスメソッドと、staticメソッドで同名のメソッドがある場合、コンパイルエラー * Double::toString ## コンパレータを使ったソート ```java= public int ageDiff(final Person other) { return age - other.age; } ``` ```java= List<Person> ascendingAge = people.stream() .sorted((person1, person2) -> person1.ageDiff(person2)) .collect(toList()); ``` ```java= List<Person> ascendingAge = people.stream() .sorted(Person::ageDiff) .collect(toList()); ``` ## コンパレータの再利用 reversedは高階関数 ```java= Comparator<Person> compareAscending = (person1, person2) -> person1.ageDiff(person2); Comparator<Person> compareDesending = compareAscending.reversed(); ``` ## 複数のプロパティによる比較 ```java= people.stream() .sorted((person1, person2) -> person1.getName().compareTo(person2.getName())); ``` より簡潔で意図が分かり易い形 ```java= people.stream() .sorted((person1, person2) -> person1.getName().compareTo(person2.getName())); ``` 年齢と名前でソートする ```java= final Function<Person, Integer> byAge = person -> person.getAge(); final Function<Person, String > byTheirName= person -> person.getName(); people.stream() .sorted(comparing(byAge).thenComparing(byTheirName)) .collect(toList()); ``` ## collect メソッドとCollectorsクラスの使用 命令型のコード スレッドセーフを意識する必要がある。 ```java= List<Person> olderThan20 = people.stream .filter(~) .forEach(person -> olderThan20.add(person)); ``` * collect() * サプライヤ * 結果を収めるコンテナの生成方法 * アキュムレータ * コンテナに単一の要素を追加する方法 * コンバイナ * コンテナと他のコンテナを結合する方法 意図が明確 スレッドセーフ。 ```java= List<Person> olderThan20 = people.stream .filter(~) .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); ``` コンニビニエンスメソッド ```java= List<Person> olderThan20 = people.stream .filter(~) .collect(Collectors.toList()); ``` * グルーピング ```Java= Map<Integer, List<Person>> peopleByAge = people.stream() .collect(Collectors.groupingBy(Person::getAge)); ``` ``` {35=[Greg - 35], 20=[John - 20], 21=[Sara - 21, Jane - 21]} ``` * 複数の基準でのグルーピング ```java= Map<Integer, List<String>> nameOfPeopleByAge = people.stream() .collect( groupingBy(Person::getAge, mapping(Person::getName, toList()))); ``` ``` {35=[Greg], 20=[John], 21=[Sara, Jane]} ``` * 蓄積処理の組み合わせ 名前の頭文字でグループ化し、各グループの最年長を抽出する。 ```java= Comparator<Person> byAge = Comparator.comparing(Person::getAge); Map<Character, Optional<Person>> oldestPersonOfEachLetter = people.stream() .collect(groupingBy(person -> person.getName().charAt(0), reducing(BinaryOperator.maxBy(byAge)))); ``` # 4.ラムダ式で設計する ## ラムダ式を使った関心の分離 * 分離前 ```java= final List<Asset> assets = Arrays.asList( new Asset(Asset.AssetType.BOND, 1000), new Asset(Asset.AssetType.BOND, 2000), new Asset(Asset.AssetType.STOCK, 3000), new Asset(Asset.AssetType.STOCK, 4000) ); int total = totalAssetValue(assets); ``` ```java= public static int totalAssetValue(final List<Asset> assets) { return assets.stream() .mapToInt(Asset::getValue) .sum(); ``` BONDだけ合計したい場合は? 重複が発生しやすい。 ```java= public static int totalBondValue(final List<Asset> assets) { return assets.stream() .mapToInt(asset -> asset.getType() == Asset.AssetType.BOND ? asset.getValue() : 0) .sum(); } ``` OOPのStrategyパターンに近い 「何を」合計するかを外だしする。 ```java= public static int totalAssetValue(final List<Asset> assets, final Predicate<Asset> assetSelector) { return assets.stream() .filter(assetSelector) .mapToInt(Asset::getValue) .sum(); } ``` ```java= totalAssetValue(assets, asset -> true); totalAssetValue(assets, asset -> asset.getType() == Asset.AssetType.BOND); ``` ## ラムダ式を使った移譲 関数をフィールド定義し、コンストラクタで受け取れるようにする。 →テスト時にモックにしやすい。 ```java= public class CalculateNAV { public BigDecimal computeStockWorth( final String ticker, final int shares) { return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares)); } public CalculateNAV(Function<String, BigDecimal> priceFinder) { this.priceFinder = priceFinder; } private Function<String, BigDecimal> priceFinder; } ``` ```java= // テスト final CalculateNAV calculateNAV = new CalculateNAV(ticker -> new BigDecimal("6.01")); // 実際の呼び出し final CalculateNAV calculateNAV = new CalculateNAV(YahooFinance::getprice); ``` * Functionインタフェースのラムダ式の中でチェック例外を投げることはできない * RuntimeExceptionでラップする ## ラムダ式を使ったデコレーション デコレーターパターン 既存のオブジェクトに新しい機能や振る舞いを動的に追加することを可能にする。 継承の代替。 ![](https://i.imgur.com/FH07Uk6.png) 以下は同じ ```java= wrapper = target.compose(next); wrapper.apply(input); ``` ```java= next.apply(target.apply(input)); ``` 複数のフィルタを追加する例 ```java= public class Camera { private Function<Color, Color> filter; public static void main(String[] args) { final Camera camera = new Camera(); final Consumer<String> printCaptured = (filterInfo) -> System.out.println(String.format("with %s: %s", filterInfo, camera.capture(new Color(200, 100, 100)))); // フィルターなし printCaptured.accept("no filter"); // フィルターを設定 camera.setFilters(Color::brighter); printCaptured.accept("bright filter"); // 複数のフィルターを設定 camera.setFilters(Color::brighter, Color::darker); printCaptured.accept("bright & darker filter"); } public Color capture(final Color inputColor) { final Color processedColor = filter.apply(inputColor); // 色の処理 return processedColor; } public void setFilters(final Function<Color, Color>... filters) { // 複数のフィルタをイテレートし、チェーン化する filter = Stream.of(filters) .reduce((filter, next) -> filter.compose(next)) // 渡された色をそのまま返す //.orElse(color -> color); .orElseGet(Function::identity); } public Camera() { setFilters(); } } ``` ``` with no filter: java.awt.Color[r=200,g=100,b=100] with bright filter: java.awt.Color[r=255,g=142,b=142] with bright & darker filter: java.awt.Color[r=200,g=100,b=100] ``` ## defaultメソッド * ルール * サブはスーパーのデフォルトを継承する * サブの実装が優先される * クラスの実装は全てのデフォルトに優先する *** 抽象クラスは状態を持てるがインタフェースは状態を持てない** ## ラムダ式を使った流暢なインタフェース * メソッドチェーンの問題点 * 意図した順序を保証できない * new により可読性が下がる * よくわからん ```java= public class MailBuilder { public MailBuilder from(final String address) { return this; } public MailBuilder to(final String address) { return this; } public MailBuilder subject(final String line) { return this; } public MailBuilder body(final String message) { return this; } public void send() { } public static void main(String[] args) { new MailBuilder() .from("xxx@xxx.com") .to("yyy@yyy.com") .subject("build modification") .body("ver 0.1") .send(); } } ``` * ラムダ式によるメソッドチェーン * オブジェクトのスコープがブロック内に限定される * 呼び出し側で new が必要ない * send が終端であることが保証される * その他の順序は保証されてない? ```java= public class FluentMailer { // 意図しないインスタンス化を防止する private FluentMailer() { } public FluentMailer from(final String address) { return this; } public FluentMailer to(final String address) { return this; } public FluentMailer subject(final String line) { return this; } public FluentMailer body(final String message) { return this; } public static void send(final Consumer<FluentMailer> block) { final FluentMailer mailer = new FluentMailer(); block.accept(mailer); // ここで実際の送信 } public static void main(String[] args) { FluentMailer.send(mailer -> mailer .from("xxxx@xxxx.com") .to("yyyy@yyyy.com") .subject("build notification") .body(" much better")); } } ``` # 5.外部リソースを扱う # 6.遅延させるということ ## 初期化の遅延 重いオブジェクトの生成を遅らせることで * おなじみのアプローチ スレッドセーフでない 2つ以上のスレッドが同時にgetHeavy()を実行した場合、1つのスレッド二月1つのHeavyインスタンスが生成される。 ```java= private Heavy heavy; public Heavy getHeavy() { if (heavy == null) { heavy = new Heavy(); } return heavy; } ``` 排他制御すると、競合は解決するが、オーバーヘッドが発生する ```java= public synchronized Heavy getHeavy() { if (heavy == null) { heavy = new Heavy(); } return heavy; } ``` ###### tags: `Java`