8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

curry化と部分適用をJavaで ~命名編~

Last updated at Posted at 2022-12-22

この記事は アイスタイル 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日となりました。
アイスタイルではいろんなジャンルの記事を書いているのでよかったらご覧になってください。

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?