第一級関数の使い方が分からないJavaディベロッパに教えたいその使いどころ:返却値編
目次
こんにちは、アーキテクトのQZ西垣です。
アトラスでは、開発言語として主にJava8を使用しています。
このブログの執筆時から2ヶ月ほど前、2017年9月に関数型言語的機能が強化されたJava9が発表されました。
関数型言語的機能というと、代表的なのがラムダ式による第一級関数ですが、皆さんは使いこなしているでしょうか。
今回は、そんな第一級関数の使いどころが今ひとつ分からないという方のために、メソッドの返却値に限定して解説します。
第一級関数って?
第一級関数(Wikipedia)
第一級関数とは、変数・関数の引数・関数の戻り値として、関数を扱うことを指します。
Javaでは第一級関数を以下のように扱います。
- 関数とはメソッドと同義である
- 第一級関数自体は、ラムダ式・メソッド参照・コンストラクタ参照として記述する
- 第一級関数の型として、関数型インターフェイスを使用する
以下にJavaでの第一級関数の例を示します。
1 2 3 4 5 6 |
class Main { public static void main(String[] args) { IntUnaryOperator calc = x -> x * 2; // ① System.out.println(calc.applyAsInt(4)); // ② } } |
上述の例では、①の IntUnaryOperator
が、int
型の引数を1つ取り、int
型の値を返却する、関数型インターフェイスです。
②の calc.applyAsInt
で、①で定義した第一級関数を呼び出します。
つまり、calc.applyAsInt(4)
の結果、8が返却されます。
部分適用
第一級関数を使用することにより、関数に対し一部の引数だけを適用できます。
これを部分適用といいます。
部分適用を使用すれば、コードから冗長性を取り除けます。
以下に例を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Main { public static void main(String[] args) { System.out.println(calc1(1, 2, 3, 4)); // ① System.out.println(calc1(1, 2, 3, 5)); // ② IntUnaryOperator f2 = calc2(1, 2, 3); // ③ System.out.println(f2.applyAsInt(4)); // ④ System.out.println(f2.applyAsInt(5)); // ⑤ } private static int calc1(int w, int x, int y, int z) { return w + x + y + z; } private static IntUnaryOperator calc2(final int w, final int x, final int y) { return z -> calc1(w, x, y, z); } } |
①②は第一級関数を使用しない例です。
メソッド calc1
に対して4つの引数を指定しています。
しかし、うち3つは全く同じ内容なので、明らかに冗長です。
③④⑤は第一級関数を使用した例です。
③で メソッド calc2
に対して3つの引数を指定してしています。
返却値の型は IntUnaryOperator
となっています。
これは関数型インターフェイスなので、第一級関数が返却されます。
つまり変数 f2
には第一級関数が格納されます。
④⑤では、f2
のメソッド applyAsInt
に対して、引数を1つだけ指定しています。
f2
には③で既に3つの引数を適用しているため、あらためて④⑤で指定する必要がないのです。
このように、同じ引数を複数回指定するという冗長性をコードから取り除くことができます。
また、第一級関数を返却する第一級関数を使用すれば、より柔軟かつ強力に部分適用ができます。
クロージャ
クロージャ(Wikipedia)
クロージャは、Wikipediaでは以下のように定義されています。
引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。
定義だけでは分かりにくいので、以下に例を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Main { public static void main(String[] args) { IntUnaryOperator f3 = calc3(1, 2, 3); // ⑥ System.out.println(f3.applyAsInt(4)); // ⑦ System.out.println(f3.applyAsInt(5)); // ⑦ } private static IntUnaryOperator calc3(final int w, final int x, final int y) { final int multi = w * x * y; // ⑧ final int add = w + x + y; // ⑧ return z -> z * multi + add; // ⑨ } } |
⑥で calc3
を呼び出すと、⑧が実行され、calc3
内のローカル変数 multi
と add
に値が格納されます。
⑦で f3
のメソッド applyAsInt
を呼び出すことによって、⑨のラムダ式が実行されます。
ラムダ式では multi
と add
を使用していますが、このときこれらの変数の値は保持されたままになっています。
⑦が実行されるたびに⑧が実行されるわけではありません。
このように、ラムダ式内でラムダ式外のローカル変数を参照した場合、ローカル変数の値が保持されることをクロージャと呼びます。
プログラムでは、DBからフェッチしたレコードの内容など、生成に時間がかかる変数を扱うことが多々あります。
パフォーマンスの向上を望むなら、Java7以前であれば、その変数をメソッド間で持ち回ったり、クラス変数にキャッシュしたりする必要がありました。
クロージャを使用すれば、対象の変数を最小限のスコープで管理できるようになります。
なお、Javaの場合、参照するローカル変数は final
が必須となるため、厳密にはクロージャではありません。
Java8のlambda構文がどのようにクロージャーではないか
終わりに
第一級関数はJava7以前には存在しない新しい概念です。
とっつきやすいものとも言えません。
しかし、理解し使いこなせば、第一級関数はコーディングにおいて極めて強力な武器になります。
このブログが少しでもその助けになれば幸いです。