この記事は アイスタイル Advent Calendar 2022 23日目の記事です。
はじめに
みなさんこんにちは。
入社4年目、アイスタイルのバックエンドサービスを担当しているkuriyamayです。
アドベントカレンダー2本目の投稿になります。
1本目はcurry化と部分適用をJavaで
という内容で記事を書きました。
今回は既存メソッドをcurry化・部分適用して、さらに命名にも気をつかった記述にしたいと思います。
curry化や部分適用とは?という方は1本目の記事をご覧ください。
既存メソッドのcurry化と部分適用
2つの引数を受け取り結果を返すメソッドがあります。
例のため、分かりやすいように2つのStringを受け取り結合して返すだけの簡単な処理です。
public String concat(String left, String right) {
return left + right;
}
普通に使ってみる
入力値に対して"prefix: "
文字列をつける場合はこのような記述になりますね。
処理する回数分"prefix: "
をconcatメソッドに渡しているので冗長な書き方になっています。
var prefixed =
Stream
.of("targetA", "targetB", "targetC")
.map(target -> this.concat("prefix: ", target))
.collect(toList()); // ["prefix: targetA", "prefix: targetB", "prefix: targetC"]
curry化・部分適用
BiFunction<String, String, String>
を引数に持ち、xを受け取り、yを受け取り結果を返すcurryメソッドを作ります。
このcurryメソッドにconcatメソッドをメソッド参照で渡してcurry化・部分適用できるようにします。
// curryingのメソッド
public static Function<String, Function<String, String>> curry(BiFunction<String, String, String> biFunction) {
return left -> right -> biFunction.apply(left, right);
}
// currying & 部分適用
var partialApply = curry(this::concat).apply("prefix: ");
var prefixed =
Stream
.of("targetA", "targetB", "targetC")
.map(partialApply)
.collect(toList()); // ["prefix: targetA", "prefix: targetB", "prefix: targetC"]
this::concat
は型がBiFunction<String, String, String>
になり関数インターフェースとして扱うことができます。
これで"prefix: "
を部分適用した関数が作れました。
名前を付ける
既存メソッドをcurryingして部分適用もできましたが、部分適用するときのapply()
という名前は何をしているのか分かりづらいです。
名前をしっかり付けることでこのメソッドを使う人や、コードを読む人のストレスを少しでも減らしてあげることができますね。
interfaceを定義
- Curry
- LeftPartialApply
というinterfaceを定義し、先ほど作ったcurryメソッドの戻り値をCurry
にします。
Curryには入力を受け取りLeftPartialApplyを返す抽象メソッドleft
を定義、LeftPartialApplyも入力を受け取り結果を返すright
を定義します。
どちらも関数インターフェースのFunction<T, R>
を継承させます。
下記のような形ですね。
public static Curry curry(BiFunction<String, String, String> biFunction) {
return left -> right -> biFunction.apply(left, right);
}
public interface Curry extends Function<String, LeftPartialApply> {
LeftPartialApply left(String s);
@Override
default LeftPartialApply apply(String s) {
return this.left(s);
}
}
public interface LeftPartialApply extends Function<String, String> {
String right(String s);
@Override
default String apply(String s) {
return this.right(String);
}
}
このような使い方になります。
var leftPartialApplyForConcat = curry(this::concat).left("prefix: ");
var prefixed =
Stream
.of("targetA", "targetB", "targetC")
.map(leftPartialApplyForConcat::right)
.collect(toList()); // ["prefix: targetA", "prefix: targetB", "prefix: targetC"]
concatの左側に部分適用させるのでcurryに続くメソッド名はleft
としました。
部分適用した関数を使うときはconcatの右側に値が渡るのでこちらはright
としています。
left/right
で表現することでコードを読んだときに直感的に分かるようになりました。
一般化する
上記は型がStringなので一般化します。
public static <T, U, R> Curry<T, U, R> curry(BiFunction<T, U, R> biFunction) {
return left -> right -> biFunction.apply(left, right);
}
public interface Curry<T, U, R> extends Function<T, LeftPartialApply<U, R>> {
LeftPartialApply<U, R> left(T t);
@Override
default LeftPartialApply<U, R> apply(T t) {
return this.left(t);
}
}
public interface LeftPartialApply<U, R> extends Function<U, R> {
R right(U u);
@Override
default R apply(U u) {
return this.right(u);
}
}
使用するときの記述は変わらずです。
var leftPartialApplyForConcat = curry(this::concat).left("prefix: ");
var prefixed =
Stream
.of("targetA", "targetB", "targetC")
.map(leftPartialApplyForConcat::right)
.collect(toList()); // ["prefix: targetA", "prefix: targetB", "prefix: targetC"]
おまけ
左側ではなく右側を部分適用したいときもきっとありますよね。
そんなユースケースに備えてどちらからでも部分適用できるようにします。
// Right
public static <T, U, R> RightCurry<T, U, R> rightCurry(BiFunction<T, U, R> biFunction) {
return left -> right -> biFunction.apply(right, left);
}
public interface RightCurry<T, U, R> extends Function<U, RightPartialApply<T, R>> {
RightPartialApply<T, R> from(U t);
@Override
default RightPartialApply<T, R> apply(U u) {
return this.from(u);
}
}
public interface RightPartialApply<T, R> extends Function<T, R> {
R left(T t);
@Override
default R apply(T t) {
return this.left(t);
}
}
// Left
public static <T, U, R> LeftCurry<T, U, R> leftCurry(BiFunction<T, U, R> biFunction) {
return left -> right -> biFunction.apply(left, right);
}
public interface LeftCurry<T, U, R> extends Function<T, LeftPartialApply<U, R>> {
LeftPartialApply<U, R> from(T t);
@Override
default LeftPartialApply<U, R> apply(T t) {
return this.from(t);
}
}
public interface LeftPartialApply<U, R> extends Function<U, R> {
R right(U u);
@Override
default R apply(U u) {
return this.right(u);
}
}
これで右側左側、どちらからでも部分適用できるようになりました。
var leftPartialApplyForConcat = leftCurry(this::concat).from("prefix: ");
var prefixed =
Stream
.of("targetA", "targetB", "targetC")
.map(leftPartialApplyForConcat::right)
.collect(toList()); // ["prefix: targetA", "prefix: targetB", "prefix: targetC"]
var rightPartialApplyForConcat = rightCurry(this::concat).from(": suffix");
var suffixed =
Stream
.of("targetA", "targetB", "targetC")
.map(rightPartialApplyForConcat::left)
.collect(toList()); // ["targetA: suffix", "targetB: suffix", "targetC: suffix"]
最後に
既存メソッドをcurry化して部分適用できるようにし、さらに使いやすさを考慮した名前付けもやってみました。
curry化・部分適用についてもっと知りたいと思った方は是非お手元の環境でやってみてください。
関数への理解も進みエンジニアとしての幅も広がるはずです。
アドベントカレンダーも残すところあと2日となりました。
アイスタイルではいろんなジャンルの記事を書いているのでよかったらご覧になってください。