今年でちょうど10年ですね。10年もたったのですから、ラムダ式を当たり前のように使っている開発者も多いとは思いますが、いまだに苦手と感じている方がいらっしゃるのも事実だと思います。ラムダ式が導入後にJavaを使い始めた方でも苦手という方がいるんですよね。ラムダ式は、Javaで関数型プログラミングの考え方が導入された端緒です。APIをはじめ、今では関数型プログラミングに関する機能がJavaではどんどん増えています。Classパターンマッチングなど。構成するのに使われます。標準のAPIだと、次のようなAPIがあります。11で導入されましたが、Flowを使用して宣言的にHTTPのクライアントを記述します。Quarkusなど関数型プログラミングの考えを使うライブラリやフレームワークも増えています。これからはJavaを使っていても関数型プログラミングからは逃れられなくなっているのです。関数型プログラミングの考えを取り入れたプログラミングスタイル、つまり今までの手続き的な記述から宣言的な記述に変えていかなくてはなりません。いうと、今までのJavaはどうしても文で考えがちなのですが、そうではなく式を使って記述することを意識していきたいのです。ラムダ式とはさて、そのラムダ式ですが、端的にいえば関数です。名前はないので、無名関数ということができます。無名関数といっても、メソッドと同じようなものです。Specificationのラムダ式の説明の一番はじめには、次のような記述があります。15.27)つまり、メソッドが書ければラムダ式は書けるはずなのです。ラムダ式は通常のメソッドとは異なり、クラスに属していなかったり、ラムダ式の外側のクラスの状態にアクセスすることも制限されていたりするので、メソッドと同じというわけではありません。それは今までの手続き的な記述にとらわれてしまっているからではないでしょうか。手続き的な記述には読みにくいポイントや、バグを発生しやすくしてしまうポイントがあります。代表的なポイントを以下に示しました。1点目の複数の処理を一緒に書けてしまうというのは、処理を十分に分解せずにまとめて書きがちだということです。残りの2点は変数に関することです。手続き的な記述だと、どうしてもy処理の途中経過を保持するなどの変数が使われます。途中経過を保持させるので、ミュータブルになってしまうわけです。途中経過を保持させて後に、最終的な結果を得るためには別のスコープに入ることがあり、スコープが広くなってしまいがちです。言葉で書いても分かりにくいと思うので、具体的なコードで見てみましょう。A組の生徒の平均値を算出することを考えてみます。生徒の成績は次のRecordで保持しているとします(通常のクラスでもいいのですが、こういうデータを保持させるにはイミュータブルなRecordの方が適しています)。{}StudentScoreでは生徒の名前と、クラス、成績を保持させています。平均値を求めるメソッドを次に示します。}一見、よさげに見えますが、何が問題なのでしょう。ローカル変数のsumとcountです。この2つの変数はいずれもループでの中間値を保持させるためにミュータブルになっています。ループが終わった後の最終的な結果を求めるために、ループの外側でも変数を使用します。このため、変数のスコープがメソッド全体になってしまっています。ミュータブルだと意図しない値の変更がされる可能性があります。このメソッドは行数が少ないからよいですが、行数が多いメソッドを複数人で編集していたりすると意図しない変更が起こりがちです。スコープが広い変数だとなおさらです。このメソッドでは最後にsumをcountで割って平均値を求めていますが、変数sumはループの中では合計値を保持させています。合計値を保持させる変数であるのに、最後に合計値ではない値を代入しているわけです。平均値を代入した後に、他の人が合計値だと思って変数sumを使ってしまうとバグが発生してしまいます。ループというか、繰り返し処理ってやっぱり分かりにくいと思うんですよ。慣れてしまえばパターンとして覚えてしまうので、パッと書けるとは思いますが、初心者には難しい。カウンターのインクリメントまでやっています。ループカウンターを使ったループの制御は本来の平均を求める処理とは別個のものなので、一緒にしてしまうと分かりにくくなってしまいます。(拡張for文)で書いたとしても本質的な難しさは変わりません。ループの制御はコンテナ側に任せ、ループで扱うデータに対し何を行っていくのかを分解して考えることで分かりやすさ、読みやすさは格段に向上します。これが宣言的に記述するということにつながります。for文という文ではなく、式で処理を連ねていくわけです。先ほどの平均値を求めるメソッドを宣言的に記述したのが以下のコードです。}この記述には、return以外は文が使われておらず、式で処理を記述しています。ローカル変数のaveはイミュータブルな変数になります。APIを使う側は、データをどのように処理するかだけに集中し、それをラムダ式で記述します。行っている処理自体は手続き的に記述しても、宣言的に記述しても同じです。APIを使うことで、条件、値の取り出しなどの処理をそれぞれ1つのラムダ式で記述することで、処理の流れが分かりやすくなります。いっても、今まで手続き的な記述しかしてこなかった方には、宣言的な記述はとっつきにくい感じを受けてしまうのはしかたないと思います。いって、今までの手続き的な記述に固執していたら、どんどん増えている宣言的スタイルのライブラリやフレームワークを使えなくなってしまいます。いつかは宣言的な考え方をしなくてはいけないのであれば、今がそのチャンスです。前述したように、ラムダ式は関数として扱うのが自然です。関数型インタフェースがとかを考え始めてしまうと、なかなかとっつきにくくなります。Javaにも関数型があればこんなことに悩む必要はないのですが、残念ながらJavaでは関数型はありません。しかたないので、関数型インタフェースなんてものを持ち出したわけです。ラムダ式を関数と考えるのであれば、型はそれほど重要ではありません。重要なのはシグネチャーです。戻り値の有無と、その型が重要になります。引数や戻り値の型はジェネリクスの型パラメータで定義されるので、引数の個数や戻り値の有無から考えましょうということです。数値を保持しているリストをソートするには以下のように記述します(もっと簡単に書けますけど、説明のためこう書いてます)。.toList();sortedメソッドの引数のラムダ式で要素同士を比較して、ソートの並び順を決めています。Integer>インタフェースだとしても、処理はまったく同じです。sortedメソッドの引数にするラムダ式では2つの引数が渡されて、戻り値としてintで戻すということが重要になるわけです。java.util.functionパッケージのインタフェースラムダ式は関数で、重要なのはシグネチャということですが、ではFunctionインタフェースなどのjava.util.functionパッケージで提供されている関数型インタフェースはどのように考えればよいのでしょう。インタフェース自体の用途などは気にせずに、シグネチャーを区別するためのものと割り切ると理解しやすいです。Consumer<T>のように考えるわけです。Streamインタフェースのmapメソッドの引数はFunctionインタフェースですが、その場合は引数が1つで、戻り値ありのラムダ式を書けばいいのだなと分かるわけです。ちなみに、ラムダ式を関数型インタフェースの匿名クラスの延長として考えてしまうのは危険です。それだと、いつまでたっても手続き的な考え方に執着してしまいます(実際に動作としても匿名クラスとラムダ式はまったく異なるのですが、それはブリ会議の後半のエントリーで説明します)。そんな変なことを考えずに、ラムダ式は関数として考えましょう。ラムダ式を書く時にはシグネチャーから処理を記述していきましょう。ラムダ式を書く時のTipsここまでラムダ式は関数として扱いましょうということを説明してきたわけですが、では実際にラムダ式を書く時にどうすればよいでしょう。ラムダ式は単独で使うということはほぼなく、ほとんどがライブラリやフレームワークのメソッドの引数として使います。いうことはラムダ式を単体で考えるのではなく、ライブラリやフレームワークと合わせて一緒に考えればよいということです。ちなみに、メソッドの引数や戻り値に関数を使用することを高階関数と呼びます。名前はどうでもいいのですが、メソッドの引数にラムダ式というのがラムダ式の主流の使い方ということです。Magazineのインタビューで次のように語っています。Lambdaは単なる言語機能ではなく、ライブラリも対象としています。言語機能とライブラリが一体となって、Javaプログラミング・モデルを大幅にアップグレードします。APIを合わせて策定しています。このようにラムダ式単独ではなく、ライブラリやフレームワークと一緒に使うことを前提にラムダ式を考えていきましょう。次の処理は1つだけというのは、ラムダ式はなるべくシンプルにしましょうということです。ラムダ式のボディにいろいろ処理を書いてしまうというのは、処理を正しく分解できていないということにつながり、どうしてもラムダ式のボディが手続き的になってしまいます。処理を分解して、1つのラムダ式には単純な処理を記述し、それを連ねていくというスタイルに変えていきましょう。3つめはラムダ式で扱うデータをなるべく引数だけにするということです。ラムダ式は、ラムダ式が定義されている外側のクラスのフィールドなどにもアクセスできますが、それは可読性の低下につながります。外部のデータにアクセスするのであれば、定数などに限定しましょう。4つめの処理結果を戻り値以外で戻さないというのは、3つめの外部のデータにアクセスしないにも通じます。関数なのですから、結果は戻り値だけです。最後はラムダ式だけでなく、宣言的な記述全般にいえることですが、変数はイミュータブルにしましょう。まとめさて、前半のまとめです。なんども書いていますが、ラムダ式は関数としてあつかい、宣言的な記述を行うために役立てるのがお勧めです。匿名クラスの延長として考えてしまうと、今までの手続き的な考えの枠内にとどまってしまい、宣言的な記述を書くことの妨げになってしまいます。Javaは今までの手続き的な記述から、宣言的な記述にプログラミングスタイルが変わってきています。今後もこの流れは変わりません。手続き的な考えでラムダ式を理解しようとすることはやめましょう。宣言的な記述、文から式への移行を役立てるためにラムダ式が存在するのです。関数型プログラミング」を紹介しておきます。この本はJavaからScalaへの移行を解説した書籍です。手続き的なJavaから、宣言的なJavaへ移行するにも役立つはずです。トピックはだいたい1ページで収まっており、読みやすいのもいいです。Kindleだと画像の解像度が低くて、ちょっと読みにくいのが欠点です。翔泳社さん、どうにかしてくれないですかねぇ。www.amazon.co.jpさて、ブリ会議で後半に解説したラムダ式がどのように動作するのかについては、次エントリーで紹介する予定です。