Java 8ラムダ式はクロージャーですか?

投稿のタイトルで提起された質問に対する詳細な回答は、2015年11月25日の文言でブルース・エクケルの記事に記載されています。 この記事の翻訳をここに投稿し、Javaでの関数型プログラミングについてどう思うか、そのような本の関連性を尋ねることにしました。







素敵な読書を!







要するに-もちろんはい。



より詳細な答えを出すために、まだ理解してみましょう。なぜ彼らと協力しているのでしょうか?



行動の抽象化



基本的に、ラムダ式は、実行方法ではなく、実行する計算を記述するために必要です。 従来、私たちは外部のイテレーションを使用して作業を行いました。このイテレーションでは、操作のシーケンス全体とその実行方法を明確に示していました。



// InternalVsExternalIteration.java import java.util.*; interface Pet { void speak(); } class Rat implements Pet { public void speak() { System.out.println("Squeak!"); } } class Frog implements Pet { public void speak() { System.out.println("Ribbit!"); } } public class InternalVsExternalIteration { public static void main(String[] args) { List<Pet> pets = Arrays.asList(new Rat(), new Frog()); for(Pet p : pets) // External iteration p.speak(); pets.forEach(Pet::speak); // Internal iteration } }
      
      







外部ループはfor



ループで実行され、このループはその実行方法を正確に示します。 このようなコードは冗長であり、プログラムで繰り返し再現されます。 ただし、 forEach



ループでは、各要素に対してプログラムを呼び出して(ここでは-ラムダよりも簡潔なメソッド参照を使用して)呼び出すように指示しますが、ループの仕組みを説明する必要はありません。 反復はforEach



ループレベルで内部的に処理されます。



ラムダ式の場合の「どのようにではなく、何を」というこのような動機は基本です。 しかし、クロージャを理解するためには、関数型プログラミングの動機付けをより詳細に検討する必要があります。



関数型プログラミング



ラムダ式/クロージャーは、関数型プログラミングを簡素化するように設計されています。 もちろん、Java 8は関数型言語ではありませんが、(Pythonのように)関数型プログラミングのサポートを提供します。これらの機能は、基本的なオブジェクト指向のパラダイムの上に構築されています。

関数型プログラミングの基本的な考え方は、関数の作成と操作、特に実行時に関数を作成できることです。 したがって、プログラムはデータだけでなく関数でも動作できます。 プログラマーにどんな機会が開かれるか想像してみてください。



純粋に機能的なプログラミング言語には、特にデータ不変性の他の制限があります。 つまり、変数はなく、不変の値しかありません。 一見、この制限は過剰に思えます(どのように変数なしで機能しますか?)、しかし、本質的に、値の助けを借りれば、すべてが変数の場合と同じです(確認したい-Scalaを試してください、この言語は純粋に機能的ではありません、ただしどこでも値を使用する機能を提供します)。 不変関数は引数を取り、環境を変更せずに結果を返します。 したがって、不変関数は共有リソースをブロックしないため、並列プログラミングで使用する方がはるかに簡単です。

Java 8より前のバージョンでは、実行時に1つの方法で関数を作成できました。バイトコードの生成と読み込み(これはかなり複雑で複雑な作業です)。



次の2つの機能は、ラムダ式の特徴です。



  1. 関数を作成するためのより簡潔な構文
  2. 実行時に関数を作成する機能。 その後、これらの関数を別のコードに転送したり、別のコードを操作したりできます。




閉鎖は2番目の可能性に関連しています



閉鎖とは何ですか?



閉じるとき、関数のスコープ外の変数が使用されます。 従来の手続き型プログラミングでは、これは問題になりません-変数を使用するだけです-しかし、実行時に関数の作成を開始するとすぐに問題が発生します。 この問題を説明するために、最初にPythonの例を示します。 ここで、 make_fun()



make_fun()



という関数を作成して返します。この関数は、プログラムの残りの部分で使用されます。



 # Closures.py def make_fun(): #     : n = 0 def func_to_return(arg): nonlocal n #  'nonlocal' n += arg : #     'n'    print(n, arg, end=": ") arg += 1 n += arg return n return func_to_return x = make_fun() y = make_fun() for i in range(5): print(x(i)) print("=" * 10) for i in range(10, 15): print(y(i)) """ : 0 0: 1 1 1: 3 3 2: 6 6 3: 10 10 4: 15 ========== 0 10: 11 11 11: 23 23 12: 36 36 13: 50 50 14: 65 """
      
      







func_to_return



は、スコープ内にない2つのフィールドn



およびarg



(特定のケースに応じて、 arg



はコピーまたはスコープ外の何かを参照できる)で動作することに注意してください。 Pythonデバイス自体のおかげで、非ローカル宣言は必須です。変数を操作し始めたばかりの場合、この変数はローカルであると見なされます。 ここで、コンパイラー(はい、Pythonにはコンパイラーがあり、はい、確かに、非常に限定的な静的型チェックを行います) n += arg



func_to_return



のスコープで初期化されていないnを使用するので、メッセージが生成されますエラー。 しかし、 n



nonlocal



であると言えば、Pythonは関数のスコープ外で定義されたn



を使用していると推測し、初期化されているので、すべて問題ありません。



したがって、この問題に直面しています: func_to_return



返すだけの場合、 func_to_return



の範囲外にあるn



func_to_return



ますか? 通常、nはスコープから出てアクセスできなくなると予想されますが、その場合はfunc_to_return



は機能しません。 関数の動的作成をサポートするには、 func_to_return



はnを「閉じ」て、関数が戻るまで存続するようにします。 したがって、「クロージャ」という用語。



make_fun()



をテストするために、2回呼び出して、関数の結果をxとyに保存します。 xとyがまったく異なる結果を与えるという事実は、 make_fun()



呼び出すたびに、 n



独自の閉じたストレージで完全に独立したfunc_to_return



関数がfunc_to_return



を示しています。



Java 8のラムダ式



ラムダ式を使用した同じJavaの例を考えてみましょう。



 // AreLambdasClosures.java import java.util.function.*; public class AreLambdasClosures { public Function<Integer, Integer> make_fun() { //     : int n = 0; return arg -> { System.out.print(n + " " + arg + ": "); arg += 1; // n += arg; //     return n + arg; }; } public void try_it() { Function<Integer, Integer> x = make_fun(), y = make_fun(); for(int i = 0; i < 5; i++) System.out.println(x.apply(i)); for(int i = 10; i < 15; i++) System.out.println(y.apply(i)); } public static void main(String[] args) { new AreLambdasClosures().try_it(); } } /* Output: 0 0: 1 0 1: 2 0 2: 3 0 3: 4 0 4: 5 0 10: 11 0 11: 12 0 12: 13 0 13: 14 0 14: 15 */
      
      







あいまいなこと:本当にnに変えることができますが、nを変更しようとするとすぐに問題が始まります。 エラーメッセージは次のとおりです。ラムダ式から参照されるローカル変数は最終または実質的に最終でなければなりません(ラムダ式から参照されるローカル変数は最終または実際に最終でなければなりません)。



Javaのラムダ式は値の周りでのみ閉じられ、変数の周りでは閉じられないことがわかります。 Javaは、これらの値が不変であることを要求します。まるでそれらが最終宣言されているかのようです。 したがって、そのように宣言したかどうかに関係なく、最終的なものでなければなりません。 つまり、「実際の最終」です。 したがって、Javaには「完全な」クロージャーではなく「制約のあるクロージャー」がありますが、これは非常に便利です。



ヒープ上にないオブジェクトを作成する場合、コンパイラはリンク自体が変更されないことを確認するだけなので、そのようなオブジェクトを変更できます。 例:



 // AreLambdasClosures2.java import java.util.function.*; class myInt { int i = 0; } public class AreLambdasClosures2 { public Consumer<Integer> make_fun2() { myInt n = new myInt(); return arg -> ni += arg; } }
      
      







すべてが問題なくコンパイルされますn



の定義にfinal



キーワードを挿入できることを確認してください。 もちろん、このような動きを競合に適用する場合、可変共有状態に問題が発生します。



ラムダ式-少なくとも部分的に-目的を達成できます:関数を動的に作成できるようになりました。 海外に行くとエラーメッセージが表示されますが、通常これらの問題は解決できます。 出力はPythonほど単純ではありませんが、Javaのままです。 そして、最終結果は、いくつかの制限がないわけではありません(認めましょう、Javaでの結果には制限がないわけではありません)。



これらの構造が「クロージャ」だけでなく「ラムダ」と呼ばれた理由を尋ねました。すべての表示から、これらは純粋なクロージャだからです。 「閉鎖」は失敗し、過負荷の用語だと言われました。 誰かが「本当の閉鎖」と言うとき、彼はしばしば、「閉鎖」と呼ばれる実体があった最初のマスタープログラミング言語で出会った「閉鎖」を意味します。



ここではOOPとFPの論争は見られませんが、手配するつもりはありませんでした。 さらに、ここには「短所」さえありません。 OOPはデータの抽象化に適しています(Javaがオブジェクトの操作を強制する場合でも、オブジェクトを使用してタスクを解決できるという意味ではありません)。FPは動作を抽象化するためのものです。 両方のパラダイムは有用であり、私の意見では、PythonとJava 8の両方でそれらを組み合わせるとさらに便利になります。最近、純粋に機能するHaskell言語で書かれたコンバーターであるPandocを使用する機会がありました。ポジティブな印象。 したがって、純粋に関数型の言語も太陽の下での価値があります。



All Articles