この記事は アイスタイル Advent Calendar 2022 1日目の記事です。
みなさんこんにちは。
入社4年目、アイスタイルのバックエンドサービスを担当しているkuriyamayです。
アドベントカレンダーへの参加はこれで3年連続3回目となりまして、去年・一昨年の記事はこちらです。
ここ最近はオンプレサービスをAPI Gateway + Lambdaにしたり、オンプレサービスをコンテナ化してECSで動かしたりとAWS移行を主にやっていました。
プログラミング関連のアウトプットはあまり出来ていなかったのでアドベントカレンダーでしていこうと思います。
今年は「curry化と部分適用をJavaで」について書いていきます。
目次
- curry化とは
- 部分適用とは
- Javaで書いてみる
- 応用編
- 一工夫してみる
- 最後に
curry化とは
カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
難しい概念ですね。
つまりx,yを受け取り結果を返す関数を、xを受け取り、戻り値がyを受け取り結果を返す関数にしてあげることをcurry化といいます。
実際にコードで見たほうが理解しやすいと思うので、まずはJavaScriptで書いてその後にJavaで書いていきます。
uncurry
x,yを受け取る関数
const uncurry = (x, y) => x + y;
console.log(uncurry(1, 2)) // 3
currying
xを受け取り、yを受け取り結果を返す関数
const currying = x => y => x + y ;
console.log(currying(1)(2)) // 3
第一級関数を扱える言語(Haskell/Erlang/Scala/JavaScript等)ではカリー化関数を作ることができます。
ちなみにHaskellの関数はデフォルトでカリー化されているようです。
部分適用とは
curry化を使う理由として部分適用があります。
例えば、全ての入力値に対して常に同じ処理をしたい場合、部分適用をした関数を予め作ってあげることで可読性や再利用性が高まります。
こちらも、まずはJavaScriptで書いてみます。
PartialApply
const currying = x => y => x + y ;
// 部分適用
const partialApply = currying(1);
console.log(partialApply(2)); // 3
console.log(partialApply(3)); // 4
console.log(partialApply(4)); // 5
x = 1を適用した関数を予め作っておくことで後続の処理には1つの引数だけで1 + yの結果が返ってくるようになりました。
Javaで書いてみる
Javaは第一級関数をサポートしていないので関数インターフェースを使います。
uncurry
x,yを受け取る関数
BiFunction<Integer, Integer, Integer> uncurry = Integer::sum;
uncurry.apply(1, 2); // 3
currying
xを受け取り、yを受け取り結果を返す関数
Function<Integer, Function<Integer, Integer>> currying = x -> y -> x + y;
currying.apply(1).apply(2); // 3
PartialApply
Function<Integer, Function<Integer, Integer>> currying = x -> y -> x + y;
var partialApply = currying.apply(1);
partialApply.apply(2); // 3
partialApply.apply(3); // 4
partialApply.apply(4); // 5
Javaでも関数インターフェースを使ってあげればJavaScriptと同じようなことができますね。
応用編
複数の商品価格に対して消費税(10%)を適用していくというケースで考えていきます。
uncurry
まずはcurry化しない場合はこちら。
Streamのmapが冗長に感じてしまいますね。
BiFunction<Double, Integer, Double> ADD_TAX = (tax, price) -> tax * price;
var priceA = 10000;
var priceB = 20000;
var priceC = 30000;
var prices =
Stream
.of(priceA, priceB, priceC)
.map(price -> ADD_TAX.apply(1.1, price))
.collect(toList()); // [11000.0, 22000.0 33000.0]
currying & PartialApply
では、先ほどの処理をcurry化して部分適用もしてあげましょう。
// currying
Function<Double, Function<Integer, Double>> ADD_TAX = tax -> price -> tax * price;
// 消費税を部分適用させた関数を作成
var toTaxAppliedPrice = ADD_TAX.apply(1.1);
var priceA = 10000;
var priceB = 20000;
var priceC = 30000;
var prices =
Stream
.of(priceA, priceB, priceC)
.map(toTaxAppliedPrice)
.collect(toList()); // [11000.0, 22000.0 33000.0]
冗長だったmap処理がだいぶスッキリしました。
さらに部分適用の変数に名前付けしてあげることで、Streamのmap処理がmap to tax applied price
と語るようなコードになりましたね。
一工夫してみる
curry化・部分適用で可読性の高いコードになりましたがproduction readyとは言えませんね。
もう少し工夫してみます。
先ほどのコードは価格を扱う処理が使う側で実装されていました。
これでは消費税を計算してあげる処理が至る所に書かれてしまいバグの温床になります。
価格を扱うVOを作り業務知識をそこに閉じ込めてあげましょう。
public class Price {
private Price() {
}
private static final Double TAX = 1.1;
private static final Function<Double, Function<Integer, Double>> ADD_TAX = tax -> price -> tax * price;
public static final Function<Integer, Double> TO_TAX_APPLIED_PRICE = ADD_TAX.apply(TAX);
}
使う側はこのようになります。
var prices =
Stream
.of(priceA, priceB, priceC)
.map(TO_TAX_APPLIED_PRICE)
.collect(toList());
もし消費税に変更があった場合はPriceクラスのTAXを変更してあげるだけで良くなり変更にも強い形になりました。
使う側も今の消費税率がいくらかを気にすることなく、Priceクラスに値を渡してあげるだけで良いですね。
さらに部分適用してあげた関数に対して入念にUTを書いてあげれば保守性・再利用性も高まります。
最後に
Javaを使ってcurry化と部分適用をやってみました。
このようなテクニックを覚えておくことで他の人が実装したコードを読んだときや、冗長な記述になってしまう処理を書いたときの助けになります。
良い設計をして良いコーディングが出来るようにこれからも頑張っていこうと思います。
最後まで読んでいただきありがとうございました。
引き続きアドベントカレンダーをお楽しみください。
追記(2022/12/23)
アドベントカレンダー2本目を投稿しました。
こちらもcurry・部分適用について書いています。
参考