私は最近、クロージャをサポートしている可能性のあるJavaの次のリリースについてたくさん読んでいます。クロージャが何であるかをかなりしっかりと把握しているように感じますが、それらがオブジェクト指向言語を「より良く」する方法の確かな例を考えることはできません。誰かが私に閉鎖が必要とされる(あるいは好まれる)特定のユースケースを教えてもらえますか?
19 に答える
Lisp プログラマーとして、私は Java コミュニティーが次の違いを理解してくれることを望みます:オブジェクトとしての関数とクロージャーの違い。
a)関数は、名前付きまたは匿名にすることができます。しかし、それらはそれ自体のオブジェクトになることもできます。これにより、関数を引数として渡したり、関数から返したり、データ構造に格納したりできます。これは、関数がプログラミング言語のファースト クラス オブジェクトであることを意味します。
無名関数は言語に多くを追加するものではなく、関数をより短い方法で記述できるようにするだけです。
b)クロージャーは、関数にバインディング環境を加えたものです。クロージャーは、下向きに(パラメーターとして) 渡すか、上向きに(戻り値として) 返すことができます。これにより、周囲のコードがアクティブでなくなった場合でも、関数はその環境の変数を参照できます。
a)がある言語の場合、b) をどうするかという問題が発生します。a )があってもb)がない言語があります。関数型プログラミングの世界では、a) (関数) とb (クロージャーとしての関数) が現在の標準です。Smalltalkには長い間a) (ブロックは無名関数) がありましたが、その後、Smalltalk のいくつかの方言がb) (クロージャーとしてのブロック) のサポートを追加しました。
関数とクロージャを言語に追加すると、わずかに異なるプログラミング モデルが得られることが想像できます。
実用的な観点から、無名関数は、関数を渡したり呼び出したりするための短い表記を追加します。それは良いことです。
クロージャー (関数とバインド) を使用すると、たとえば、いくつかの変数 (カウンター値など) にアクセスできる関数を作成できます。これで、その関数をオブジェクトに格納し、アクセスして呼び出すことができます。関数オブジェクトのコンテキストは、アクセスできるオブジェクトだけでなく、バインディングを介してアクセスできる変数にもなりました。これも便利ですが、変数バインディングとオブジェクト変数へのアクセスが問題になっていることがわかります: いつ何かがレキシカル変数 (クロージャでアクセスできる) になり、いつオブジェクトの変数になるべきか (スロット_)。何かがクロージャまたはオブジェクトであるべきときは? どちらも同様の方法で使用できます。Scheme (Lisp の方言) を学習している学生の通常のプログラミング演習は、クロージャーを使用して単純なオブジェクト システムを作成することです。
その結果、より複雑なプログラミング言語とより複雑なランタイム モデルが作成されます。複雑すぎる?
それらはオブジェクト指向言語をより良くするものではありません。それらは実用的な言語をより実用的にします。
OOハンマーで問題を攻撃している場合(すべてをオブジェクト間の相互作用として表す)、クロージャーは意味がありません。クラスベースのオブジェクト指向言語では、クロージャは煙が充満した奥の部屋で、作業は行われますが、その後は誰もそれについて話しません。概念的には、それは忌まわしいです。
実際には、それは非常に便利です。コンテキストを保持するための新しいタイプのオブジェクトを定義し、そのオブジェクトの「do stuff」メソッドを確立し、インスタンス化して、コンテキストにデータを入力する必要はありません...コンパイラに「見て、何を見て」と伝えたいだけです。私は今アクセスできますか?それが私が望むコンテキストであり、これが私がそれを使用したいコードです-私がそれを必要とするまで私のためにこれを保持してください。
素晴らしいもの。
最も明白なことは、run()またはactionPerformed()などと呼ばれる単一のメソッドを持つすべてのクラスの疑似置換です。したがって、Runnableが埋め込まれたスレッドを作成する代わりに、代わりにクロージャーを使用します。私たちが今持っているものよりも強力ではありませんが、はるかに便利で簡潔です。
では、クロージャが必要ですか?いいえ、持っていてよかったですか?確かに、私が恐れているように、彼らがボルトで固定されていると感じない限り。
コア関数型プログラミングの概念をサポートするには、クロージャが必要だと思います。クロージャーのサポートにより、コードがよりエレガントで構成可能になります。また、コードの行をパラメーターとして関数に渡すというアイデアも気に入っています。
クロージャーを使用してリストを操作できる、非常に便利な「高階関数」がいくつかあります。高階関数は、「関数オブジェクト」をパラメーターとして持つ関数です。
たとえば、リスト内のすべての要素に何らかの変換を適用するのは非常に一般的な操作です。この高次関数は、一般に「map」または「collect」と呼ばれます。( Groovyの *.spread 演算子を参照してください)。
たとえば、クロージャーなしでリスト内の各要素を二乗するには、おそらく次のように記述します。
List<Integer> squareInts(List<Integer> is){
List<Integer> result = new ArrayList<Integer>(is.size());
for (Integer i:is)
result.add(i*i);
return result;
}
クロージャーとマップ、および提案された構文を使用すると、次のように記述できます。
is.map({Integer i => i*i})
(ここでは、プリミティブ型のボックス化に関してパフォーマンスの問題が発生する可能性があります。)
Pop Catalin で説明されているように、「select」または「filter」と呼ばれる別の高次関数があります。これを使用して、リスト内のすべての要素を特定の条件に適合させることができます。例えば:
それ以外の:
void onlyStringsWithMoreThan4Chars(List<String> strings){
List<String> result = new ArrayList<String>(str.size()); // should be enough
for (String str:strings)
if (str.length() > 4) result.add(str);
return result;
}
代わりに、次のように書くことができます
strings.select({String str => str.length() > 4});
提案を使用します。
Groovy 構文に注目するかもしれません。これは Java 言語の拡張機能であり、現在クロージャーをサポートしています。クロージャの処理方法の例については、 Groovy ユーザー ガイドのコレクションに関する章を参照してください。
備考:
「閉鎖」という用語に関しては、おそらくいくつかの明確化が必要です。私が上に示したものは、厳密に言えば閉鎖ではありません。それらは単なる「関数オブジェクト」です。クロージャとは、それを取り囲むコードの (レキシカルな) コンテキストをキャプチャ (または「クローズオーバー」) できるすべてのものです。その意味で、現在 Java にはクロージャー、つまり匿名クラスがあります。
Runnable createStringPrintingRunnable(final String str){
return new Runnable(){
public void run(){
System.out.println(str); // this accesses a variable from an outer scope
}
};
}
Javaはクロージャを必要としません。オブジェクト指向言語は、状態を格納したりアクションを実行したりするために中間オブジェクトを使用してクロージャが行うすべてのことを実行できます(Javaの場合は内部クラス)。ただし、クロージャーはコードを大幅に簡素化し、読みやすさを向上させ、その結果、コードの保守性を向上させるため、機能として望ましいものです。
私はJavaの専門家ではありませんが、C#3.5を使用しており、クロージャはこの言語の私のお気に入りの機能の1つです。たとえば、次のステートメントを例として取り上げます。
// Example #1 with closures
public IList<Customer> GetFilteredCustomerList(string filter) {
//Here a closure is created around the filter parameter
return Customers.Where( c => c.Name.Contains(filter)).ToList();
}
ここで、クロージャを使用しない同等の例を取り上げます
//Example #2 without closures, using just basic OO techniques
public IList<Customer> GetFilteredCustomerList(string filter) {
return new Customers.Where( new CustomerNameFiltrator(filter));
}
...
public class CustomerNameFiltrator : IFilter<Customer> {
private string _filter;
public CustomerNameFiltrator(string filter) {
_filter = filter;
}
public bool Filter(Customer customer) {
return customer.Name.Contains( _filter);
}
}
これはJavaではなくC#であることは知っていますが、考え方は同じです。クロージャは簡潔にするために役立ち、コードを短くして読みやすくします。舞台裏では、C#3.5のクロージャは、例2と非常によく似た動作をします。つまり、コンパイラは舞台裏でプライベートクラスを作成し、それに'filter'パラメータを渡します。
Javaは機能するためにクロージャーを必要としません。開発者としてはクロージャーも必要ありませんが、Javaは便利で利点があるため、本番言語である言語で望ましいものであり、その目標の1つは生産性です。 。
最近、Java の次のリリースでクロージャがサポートされる可能性があるという記事をよく読んでいます。クロージャーとは何かについてはかなりしっかりと把握しているように感じますが、クロージャーがオブジェクト指向言語を「より良く」する方法の確かな例は思いつきません。
「クロージャ」という用語を使用するほとんどの人は、実際には「関数オブジェクト」を意味します。この意味で、関数オブジェクトを使用すると、並べ替え関数でカスタム コンパレータが必要な場合など、特定の状況でより単純なコードを記述できます。
たとえば、Python では次のようになります。
def reversecmp(x, y):
return y - x
a = [4, 2, 5, 9, 11]
a.sort(cmp=reversecmp)
これは、カスタム比較関数 reversecmp を渡すことによって、リストを逆順に並べ替えます。ラムダ演算子を追加すると、さらにコンパクトになります。
a = [4, 2, 5, 9, 11]
a.sort(cmp=lambda x, y : y - x)
Java には関数オブジェクトがないため、「ファンクター クラス」を使用してそれらをシミュレートします。Java では、Comparator クラスのカスタム バージョンを実装し、それを sort 関数に渡すことで、同等の操作を行います。
class ReverseComparator implements Comparator {
public compare(Object x, Object y) {
return (Integer) y - (Integer) x;
}
...
List<Integer> a = Arrays.asList(4, 2, 5, 9, 11);
Collections.sort(a, new ReverseComparator());
ご覧のとおり、これはクロージャと同じ効果をもたらしますが、よりぎこちなく、より冗長です。ただし、匿名の内部クラスを追加すると、ほとんどの問題が解消されます。
List<Integer> a = Arrays.asList(4, 2, 5, 9, 11);
Comparator reverse = new Comparator() {
public Compare(Object x, Object y) {
return (Integer) y - (Integer) x;
}
}
Collections.sort(a, reverse);
したがって、Java のファンクター クラスと無名の内部クラスの組み合わせは、真の関数オブジェクトの不足を補うのに十分であり、それらの追加が不要になると言えます。
Javaは、1.1以降、非常に面倒で限られた方法で閉鎖されています。
これらは、説明のコールバックがある場合に役立つことがよくあります。一般的なケースは、制御フローを抽象化して、外部制御フローのないクロージャでアルゴリズムを呼び出すための興味深いコードを残すことです。
簡単な例はfor-eachです(Java 1.5にはすでにそれがありますが)。現状ではJavaでforEachメソッドを実装できますが、冗長すぎて役に立ちません。
既存のJavaですでに理にかなっている例は、リソースの取得と解放が抽象化される「executearound」イディオムの実装です。たとえば、ファイルのオープンとクローズは、クライアントコードが詳細を正しく取得しなくても、try/finally内で実行できます。
何人かの人々は、クロージャーは単なるシンタックス シュガーにすぎないと言っている、またはほのめかしています。つまり、匿名の内部クラスで既に実行できることを実行し、パラメーターを渡すのをより便利にします。
これらは、Java がアセンブラーのシンタックス シュガーであるのと同じ意味で、シンタックス シュガーです (その「アセンブラー」は、議論のためにバイトコードである可能性があります)。つまり、抽象化のレベルを上げる、これは重要な概念です。
クロージャーは、関数としてのオブジェクトの概念をファーストクラスのエンティティーに昇格させます。コードの表現力を高めるものであり、定型文でコードを乱雑にするのではありません。
私の心に近い例は、Tom Hawtin によって既に言及されています。これは、RAII を Java に組み込むほぼ唯一の方法である Execute Around イディオムの実装です。閉鎖が来るかもしれないと最初に聞いたとき、私は数年前にまさにその主題に関するブログエントリを書きました.
皮肉なことに、クロージャが Java に適している (より少ないコードで表現力が向上する) というまさにその理由が、多くの Java 支持者を動揺させているのかもしれません。Javaには、「すべてを長い道のりで綴る」という考え方があります。それと、クロージャーが物事を行うためのより機能的な方法へのうなずきであるという事実 - 私はこれも良いことだと思いますが、Java コミュニティの多くが大切にしている純粋な OO メッセージを骨抜きにするかもしれません。
JDK8がリリースされようとしている今、この質問への回答を充実させることができる、より多くの情報が利用可能です。
Oracle の言語アーキテクトである Bria Goetz は、Java におけるラムダ式の現状に関する一連の論文 (まだドラフト) を公開しています。また、 2013 年 1 月頃にコードが完成し、2013 年半ば頃にリリースされる予定の次期 JDK でクロージャーをリリースする予定であるため、クロージャーについても説明します。
- The State of Lambda : この記事の最初の 1 ~ 2 ページでは、ここで提示された質問に答えようとしています。私はまだ引数が短いと感じましたが、例がたくさんあります.
- The State of Lambda - Libraries Edition : 遅延評価や並列処理などの利点をカバーしているため、これも非常に興味深いものです。
- The Translation of Lambda Expressions : 基本的に、Java コンパイラーによって行われる脱糖プロセスについて説明します。
ここ数日、この非常に興味深い質問のトピックについてよく考えてきました。まず第一に、私の理解が正しければ、Java には (匿名クラスによって定義された) クロージャーの基本的な概念が既にいくつかありますが、これから導入される新機能は、匿名関数に基づくクロージャーのサポートです。
この拡張機能により言語がより表現力豊かになることは間違いありませんが、言語の残りの部分に本当に適合するかどうかはわかりません。Java は、関数型プログラミングをサポートしないオブジェクト指向言語として設計されています。新しいセマンティクスは理解しやすいでしょうか? Java 6には関数さえありません.Java 7には匿名関数がありますが、「通常の」関数はありませんか?
私の印象では、関数型プログラミングのような新しいプログラミング スタイルやパラダイムが普及するにつれて、誰もがそれらをお気に入りの OOP 言語で使用したいと考えています。これは理解できます。新しい機能を採用しながら、使い慣れた言語を引き続き使用したいという人がいます。しかし、このようにして、言語は非常に複雑になり、一貫性を失う可能性があります.
したがって、現時点での私の態度は、OOP 用に Java 6 に固執することです (Java 6 がしばらくの間サポートされることを願っています)。また、OOP + FP に本当に興味がある場合は、次のような他の言語を検討します。 Java 7 に切り替えるのではなく、Scala (Scala は最初からマルチパラダイムとして定義されており、Java とうまく統合できます)。
Java が成功したのは、単純な言語と非常に強力なライブラリやツールを組み合わせたことが原因だと思います。また、クロージャなどの新しい機能によって Java がより優れたプログラミング言語になるとは思いません。
より良いプログラマーになるために自分自身にlispを教えようとしているJava開発者として、私はクロージャのJoshBlock提案が実装されることを望んでいます。匿名の内部クラスを使用して、データを集約するときにリストの各要素をどう処理するかなどを表現しています。抽象クラスを作成する代わりに、それをクロージャとして表すとよいでしょう。
命令型言語 (例: JavaScript、C#、今後の C++ 更新) のクロージャーは、匿名の内部クラスと同じではありません。ローカル変数への変更可能な参照をキャプチャできる必要があります。Java の内部クラスは、ローカルfinal
変数のみをキャプチャできます。
ほとんどすべての言語機能は、必須ではないと批判される可能性があります。
for
、while
、do
はすべてgoto
/上の単なる構文糖衣if
です。- 内側のクラスは、外側のクラスを指すフィールドを持つクラスに対する構文糖衣です。
- ジェネリックは、キャストに対する構文糖衣です。
まったく同じ「必須ではない」引数が、上記のすべての機能を含めることを妨げているはずです。
匿名関数でのバインディングの欠如[つまり、外部コンテキストの変数(および、囲んでいるメソッドがある場合はメソッド引数)がfinalと宣言されている場合、それらは使用可能ですが、それ以外の場合は使用できません]、その制限が実際に何を購入するのかよくわかりません。
とにかく「ファイナル」を多用します。したがって、クロージャー内で同じオブジェクトを使用することが私の意図である場合、実際には、それらのオブジェクトを囲んでいるスコープでfinalと宣言します。しかし、「closure [java aic]」に、コンストラクターを介して渡されたかのように参照のコピーを取得させることの何が問題になりますか(実際には、それがどのように行われるかです)。
クロージャーが参照を上書きしたい場合は、そうしてください。囲んでいるスコープが見るコピーを変更せずにそうします。
それが判読不能なコードにつながると主張する場合(たとえば、コンストラクターがaicを呼び出したときにオブジェクト参照が何であるかを確認するのは簡単ではないかもしれません)、少なくとも構文の冗長性を低くするのはどうでしょうか?Scala?Groovy?
可読性と保守性についてはどうですか...ワンライナークロージャーは理解とデバッグが難しくなります.imoソフトウェアは寿命が長く、言語の基本的な知識を持つ人々にそれを維持させることができます...したがって、ワンライナーよりもロジックを広げてください簡単なメンテナンスのために...一般的に、ソフトウェアのリリース後にソフトウェアの世話をするソフトウェアスターはいません...
あのベンジースミスだけでなく、あなたのやり方が大好きです...
myArray.sort{ it.myProperty }
プロパティの自然言語比較がニーズに合わない場合にのみ、示したより詳細なコンパレーターが必要です。
私はこの機能が大好きです。
Java とほぼ互換性があり、JRE 上で実行される言語である Groovy を検討することをお勧めしますが、Closures をサポートしています。