Edited at

Stream APIと関数インターフェースでカジュアルに宣言型プログラミング


想定読者層


  • Javaエンジニア 

  • Streamをあまり触ったことがない

  • 関数型ってよくわかんない

  • 宣言?煎餅とどっちが美味しいの?



下記について話します


  • 宣言型の側面から...

  • Stream API

  • 関数インターフェース

  • 宣言型プログラミングの考え方の基礎の基礎



下記については話しません


  • 設計・アーキテクチャ


    • イケてなくても直せないことあるじゃん...

    • カジュアルに導入できる部分で闘う





Stream API



そもそもStream APIってなに


  • リスト操作のAPI

  • for文のシンタックスシュガーではない

  • 並列処理を手軽に行える

  • 構文自体が宣言的な文法になっている(これ)


    • ただの「リスト操作API」として使うと勿体無い





Stream APIってどうやって使うの


  • Listから生成したStreamに対して、データを加工する関数を与えていきます

  • 文法が宣言的なので、そのまま言葉として読み上げられます

  • 例を見てみます



例1: 高身長を俺より小さくする処理

personList.stream()

.filter(p -> p.getHeight() >= 180)

.foreach(p -> p.setHeight(156));


人物のリストから,

身長180cm以上の人物を抽出して,

それぞれの身長を156cmにする



例2: ロックマンを買ったメンバ名を抽出

members.stream()

.filter(m -> m.hasMegaman11)

.map(m -> m.getName)

.collect(Collectors.toList)


メンバー一覧から,

ロックマン11を持っているメンバを抽出して,

メンバーオブジェクトから名前に加工して

リストを生成する



関数インターフェース



そもそも関数IFってなに


  • メソッドを一つだけ定義したInterface

  • ラムダ式で簡単に実装できる

  • 正体は匿名クラスのシンタックスシュガー


    • 引数以外の外部変数はfinalしか利用できない

    • = 関数IFはクロージャではありません





関数IFってどうつかうの


  • ラムダ式で実装します

  • メソッド内で使ったり


    • 擬似クロージャみたいに使える

    • 説明変数とかにも便利な気もする



  • 高階関数(メソッド)に渡したりする


    • Streamはこれです



  • 4つの標準関数IFを見てみます



例1: int型の引数を倍にして返す

// Function<引数の型,戻り値の型>

Function<Integer,Integer> dbl = (i) -> i*2;
int result = dbl.apply(2); // 4


例2: 奇数の場合にtrueを返す

Predicate<Integer> isOdd = (i) -> i % 2 == 1;

isOdd.test(1); // true
isOdd.test(2); // false



例3: hydeの身長をセットする

// Consumerは値を返さない

Consumer<AtomicInteger> setHydeHeight = (i) -> i.set(156);
AtomicInteger myHeight = new AtomicInteger(178);
setHydeHeight.accept(myHeight); // hydeの身長は156cm


例4: 引数なしで値を返す関数IF

// Supplierは引数を受け取らない

final int value = 10;
Supplier<Integer> getValue = () -> value * 3;
int num = getValue.get();



で、結局なにができるの?



コードを宣言的に記述できます



宣言的って何だよ



例で理解する宣言型と命令型


  • keyとvalueを持つlistから、値が5以上のデータを取得する

  • 宣言型の例: SQL

    select * from list where value > 5;

  • 命令型の例: Java

List<Data> result = new ArrayList();

for(Data data: list){
if(data.value > 5) result.add(data);
}



全エンジニアが実現したいこと


  • 業務ロジックをそのままコードにしたい

  • コードを読んで業務ロジックを理解したい

  • ストリームと関数型で近づけるんじゃね?



StreamAPI + 関数IFで宣言的に


  • 例1: 販売可能な食料品一覧の作成

  • 食料品情報の全量が渡される

  • 下記を満たす場合に販売可能とする


    • 賞味期限が当日以降

    • 商品種別ごとに販売可能な条件が異なります

    • 通常商品の場合: 販売可能フラグがtrue

    • 季節商品の場合: 季節内フラグがtrue

    • その他の商品種別: 販売不可





例1: 販売可能な食料品一覧の作成

// 実装をそのまま言葉にします。

// 商品種別ごとの販売可能条件を定義する
// 商品種別が通常商品なら販売可能フラグを条件とする。
// 商品種別が季節商品なら季節内フラグを条件とする。それ以外はfalseを返す。
Predicate<Item> filterEachItemType =
i -> i.type == NORMAL ? i.isSalable
: i.type == SEASON ? i.isInSeason
: false;

// 食料品情報の全量から期限日が当日以降のものを抽出
// 更に商品種別ごとの販売可能条件で抽出する
// 抽出されたデータを販売可能商品リストとする
List<Item> salableList =
itemList.stream().filter(i -> i.expire >= today)
.filter(filterEachItemType)
.collector(Collectors.toList());



例1の命令型バージョン

// 実装をそのまま言葉にします。

// 販売可能商品リストを宣言
// 商品ごとに下記を繰り返し行う
// 賞味期限が当日以前ならスキップ
// 商品種別が通常商品かつ販売可能フラグがtrueなら販売可能商品リストに追加
// 商品種別が季節商品かつ季節内フラグがtrueなら販売可能商品リストに追加
List<Item> salableList = new ArrayList<>();
for(Item item: itemList){
if(item.expire < today) continue;
if(item.type == NORMAL && item.isSalable){
salableList.add(item);
}else if(item.type == SEASON && item.isInSeason){
salableList.add(item);
}
}



メリットまとめ


  • 業務仕様と設計と実装が一致する


    • 仕様バグを減らせる

    • 自然な実装になるので変更,機能追加が容易

    • DDDに繋がっていきます



  • コードから状態が排除される


    • (カウンタやフラグ,一時的な変数など)

    • 業務ロジックに集中できる





とりあえず目指したいこと


  • 完璧な宣言型,関数型は目指さなくてよい

  • ポリシー等は決めず、「コードで仕様を表現する」意識から始めてみる


    • 「形だけ」は絶対に避けたい

    • 詳しい人がちゃんとレビューする



  • その武器としてのStreamや関数IF

  • 「コードで仕様を表現する」でいえば、単純にメソッドを分けて実装を隠蔽するのもいいしね



以上