Java
java8

ラムダ式で3つ以上の引数に対応しよう

More than 1 year has passed since last update.

引数が2つ以下の場合

Java8既存のFunctionインターフェースを使えば、うまく対応できます。
例:

// 引数が1つしかない
Function<String, String> func1 = (str1) -> str1+"!";
System.out.println(func1.apply("Aha")); //Aha!

// 引数が2つしかない
BiFunction<String, String, String> combineStrings = (str1, str2) -> str1+str2;
System.out.println(combineStrings.apply("Hello ", "World")); //Hello World

なお、既存のFunctionインターフェースを分かりやすくまとめてくれた図表を見つけましたので、転載します

Java 8 Functional Interface Naming Guide

引数が3つ以上の場合

解決法1: 自作関数型インターフェース

Java8のFunctionインターフェースにはインプットが3つ以上のインターフェースが提供されていません。
ならば、Functionインターフェースを自作してみましょう。
実は、ありがたいことに、Java8がただ単純にインプットが3つ以上の関数型インターフェースを提供してないだけで、(少なくてもOracleとOpenJDKのJavaでは)複数の引数へ対応する準備が整っています。どういうことでしょうか?まず、BiFunctionのソースを見てみましょう。

@FunctionalInterface
public interface BiFunction<T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     */
    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}

肝心なのはR apply(T t, U u);だけです。このapply関数がjava.lang.reflect.Proxy.KeyFactory#applyで実装されています。

    private static final class KeyFactory
        implements BiFunction<ClassLoader, Class<?>[], Object>
    {
        @Override
        public Object apply(ClassLoader classLoader, Class<?>[] interfaces) {
            switch (interfaces.length) {
                case 1: return new Key1(interfaces[0]); // the most frequent
                case 2: return new Key2(interfaces[0], interfaces[1]);
                case 0: return key0;
                default: return new KeyX(interfaces);
            }
        }
    }

ご覧のように、KeyXが存在していて、その中でfor文を使って、interfaceごとに読み込み、レファレンスを作っています。なので、自作関数型インターフェース内でFunctionやBiFunctionと似ている感じでapplyを使えば良いでしょう。

@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v); // KeyX should be called
}

そして、このTriFunctionを使って、3つ以上の引数が存在するケースに対応すると、

TriFunction<String, Integer, Integer, String> newSubString = (str, start, number) -> 
  str.substring(start-1, str.length() <= start-1+number? str.length(): start-1+number);

String f = newSubString.apply("Hello World", 2, 7);
System.out.println(f); // ello Wo

うーん、悪くありません。しかし、もし単純な処理を行いたいだけであれば、わざわざインターフェースを作るなんか少し面倒臭いでしょう。他に方法ないでしょうか。

解決法2: カリー化

JavaScriptの経験者であれば、既に思い付いたかもしれません。カリー化すればいいです。では、カリー化とは何でしょうか?

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。

Wikipediaより

要するに、Java 8では、関数が変数のように扱うことができますので、戻り値で関数を返す手法が利用できます。

Function<String, Function<Integer, Function<Integer, String>>> newSubString = (str) -> (start) -> (number) 
  -> str.substring(start-1, str.length() <= start-1+number? str.length(): start-1+number);


String f = newSubString.apply("Hello World").apply(2).apply(7);
System.out.println(f); // ello Wo

一回目、二回目では関数が返され、そして実行スコップから出ていない為、前の変数がクロージャーで持っていて、次の処理でも使用されます。
こんな感じで既存インターフェースを使うだけで、複数の引数へ対応できるようになりました。