遅延評価いうなキャンペーンとかどうか
遅延評価については以前も書いてるんですが、そのときは結論なしでした。
が、ちょっと考えるところがあって、言語を Java に絞って自分の考えを明確にしておきます。
結論から書きましょう。
「Java(とC#) で遅延評価って書いてあるものは遅延評価ではない」です。
Java における「評価」とは
まず一番最初に、Java で「評価」って言うと、どういうことを指すのかを確認しておきます。
言語仕様の該当部分を要約すると、こんな感じでしょうか。
プログラム中の式を評価すると、結果は
- 変数
- 値
- 無し
のうちのどれかとなる。
評価した結果が値になる、というのはいいでしょう。それ以外の 2 つを軽く説明します。
評価の結果が「変数」とは?
コメント欄で指摘が入っています。
代入の結果は変数ではありません(15.26)。
結果が変数となるのは、ローカル変数、現在のオブジェクトやクラスの変数、フィールドアクセス、配列アクセスを評価した場合です。
調べ直したところ、確かに「代入式の結果自体は変数ではない」とありました。以下の記述は誤りですので、無視してください。
評価の結果が変数になる場合というのは、例えば次のような場合です。
String line; while ((line = in.readLine()) != null) { ... }
このコードにおいて、(line = in.readLine()) は評価されると、line という変数となります。そして、 line != null として評価され、真偽値となります。
評価の結果が「無し」とは?
評価の結果が無しになる場合というのは、void を戻り値とするメソッドを呼び出した場合に限られます*1。
例えば、
PrintStream ps = System.out;
ps.println("hello");
というコードにおいて、ps.println("hello") は評価されても結果としては「無し」となります。
式を評価しようとしたら例外が発生した、ということではないので注意してください。
まぁ、このエントリを読む分にはどうでもいいことですが・・・
完全に蛇足ですが、評価の完了は「通常の完了」と「急な完了」に分けられており、例外が発生した場合は「急な完了」となります(言語仕様の 15.6 より)。
となると、Java での評価は、
- 通常の完了
- 変数
- 値
- 無し
- 急な完了
のように分けられる、ということですかね。
「(遅延)評価」の誤用の例
このエントリを書くきっかけとなったものですが、
Java Advent Calendar 1 日目 - Project Lambda の遅延評価
などです。
例えば、
というのも、Stream インタフェースで提供されているメソッドは遅延評価されるからなのです
Java in the Box Annex: Java Advent Calendar 1 日目 - Project Lambda の遅延評価
という記述は「Stream インタフェースで提供されているメソッド」が式ではないため、(Java 8 で evaluation の意味が変わらない限り) 評価という言葉の誤用です。
「メソッド(起動式)は遅延評価される」と補えば、誤用ではないということもできるでしょうが、この文が言いたいのはそこではないのでやはり駄目でしょう*2。
遅延評価という言葉
この言葉が lazy evaluation を指すのか delayed evaluation を指すのかというのはひとまず置いておきましょう*3。
ここで問題にしたいのは、「遅延評価」という言葉を、「遅延リスト」のことと思って使っている人が多いのではないか、という点です。
これ、Java プログラマに限らず結構な人がそう思ってるような気がします。
少なくとも Java の言語仕様では式を値に変換することを評価と呼んでいますので、Java で評価ですらないものを遅延評価と呼んでしまう(しかも Java 界でもかなりすごい人が)のはよくないのでは、と思うわけです。
ということで、「遅延評価いうなキャンペーン」どうですかね。
や、とくに何をやるわけでもないですが。
遅延リスト
では、遅延リスト*4は何を遅延しているのでしょうか。
それは、「要素の計算」です*5。
上記ブログエントリから、例を拝借してみてみましょう(コメントは消してあります)。
List<Integer> nums = Arrays.asList(0, 1, 2, 3, 4, 5); Stream<Integer> stream = nums.stream(); Stream<Integer> stream2 = stream.filter(s -> s%2 == 0); stream2.forEach(s -> System.out.println(s));Java in the Box Annex: Java Advent Calendar 1 日目 - Project Lambda の遅延評価
この例で、3 番目の文に注目します。
// sという名前はあまりよろしくないので、nにした Stream<Integer> stream2 = stream.filter(n -> n%2 == 0);
この文を実行(評価ではないですよ)すると、stream2 には stream から偶数のみを取り出す Stream が格納されます。
偶数のみを取り出「した」Stream ではなく、偶数のみを取り出「す」Stream と表現したのがミソです。
そう、stream2 はこの時点ではまだ、stream をフィルタしておらず、「フィルタすることを予約した」とでも言うべき状態なのです。
この stream2 から実際に要素を取り出そうとするまで、フィルタという計算が遅延されるわけですね。
ここで注意してほしいのですが、この文に含まれる式で「評価されるべき式は評価されきっている」のです。
つまり、どの式も遅延評価されません。
それでは、文をばらして式にし、評価の結果どんな値になっていくのかを見てみましょう。
ローカル変数宣言文
上記の文は、全体としてローカル変数宣言文という種類の文に分類されます。
ローカル変数宣言文は、乱暴に言うと次の形の構文のことです(厳密には違うので、詳しく知りたければ 14.4 Local Variable Declaration Statements をどうぞ)。
型 変数名 = 式;
イコールの右側に式が出てきました。
上の文を当てはめると、型は Stream
メソッド起動式
stream.filter(n -> n%2 == 0) はメソッド起動式です。
メソッド起動式は
一次式.メソッド名(引数リスト)
の形をしており、stream が一次式、filter がメソッド名、n -> n%2 == 0 が引数リストです(厳密には 15.12 をどうぞ)。
引数リストは、
式 引数リスト, 式
のどちらかであり、今回は最初の方ですね。
つまり、n -> n%2 == 0 も式です。
そして、メソッド起動式を評価するためには一次式の評価と、引数リスト中の式の評価が必要となります(メソッド名は式ではないので、評価の対象ではありません)。
一次式は評価すると普通に値になるので、問題ないでしょう。
ここでは、stream を評価すると、stream という変数に格納されている Stream オブジェクトという値になります。
ラムダ式
さて、n -> n%2 == 0 を評価する必要がありますが、ラムダ式はまだ言語仕様として公式にリリースされていません。
そこで、意味的に同じものに置き換えて考えましょう。
public interface Filter<T> { boolean apply(T t); }
こんなインターフェイスがあったとして、filter が
public Stream<T> filter(Filter<T> filter)
のようなシグネチャを持っていたとします*6。
すると、次の 2 つの式は同じことを意味します。
/* Stream<Integer>なstreamがあったとして */ stream.filter( // 1. ラムダ式 n -> n%2 == 0 ) stream.filter( // 2. クラスインスタンス生成式 new Filter<Integer>() { public boolean apply(Integer n) { return n%2 == 0; } } )
つまり、ラムダ式は、クラスインスタンス生成式(15.9)とみなすことができます。
クラスインスタンス生成式は、
new 型名(引数リスト) クラス本体
となっており、型名は Filter
何をしたかったかというと、メソッド起動式を評価するために、メソッドの引数リストであるラムダ式を評価して値にすることでした。
ここで、ラムダ式を値にすると何になるかはもう明白ですね。Filter オブジェクトです。
これで一次式も引数リストも評価が終わりましたので、メソッド起動式の評価に移れます。
メソッド起動式を評価すると、起動対象となるメソッドの本体を実行し、その結果がメソッド起動式の値となります。
字面に出てくる評価すべき式をすべて評価しきりましたが、ここまでに遅延された評価(値が必要になるまで評価が保留された式)は一つもありませんでした。
Java 8 で導入予定の Stream は、遅延リストであっても遅延評価ではないということです。
n%2 == 0 が残っているじゃないか?
ラムダ式の本体の式の評価が行われていないのは、それが「まだ評価すべきではない式」だからです。
これを遅延評価と呼ぶのであれば、全てのラムダ式を遅延評価と呼ぶことになります。
例えば、
stream.forEach(n -> System.out.println(n));
とか、
Integer found = stream.findFirst(n -> n < 5);
とかも遅延評価だ、ということになりますが、これはさすがに変じゃないでしょうか。
forEach メソッドを実行すると、stream の要素全てを計算してしまいますし、findFirst も見つかるまでの要素は計算してしまいます。
少なくとも、引き合いに出したエントリではこれらのメソッドは「遅延評価されずに即時評価される」とありましたので、今回はこれを遅延評価とするという意見は取り上げません。
遅延リストの実装パターン
ということで、先のブログのエントリは遅延評価実現のための手法の解説ではなく、遅延リストを実装するための手法の解説ということになります。
これは Iterator を使った遅延リストの実装パターンとでも呼べるでしょう。特別に名前を付けたければ、「遅延評価」などと既存の用語の使いまわしをせず、遅延計算パターンとでも言えばいいんじゃないでしょうか。
用語の使いまわしは、変数名の使いまわし同様、可能な限り避けるべきです。
皆さんも、安易に「遅延評価」言わないようにしましょう。
じゃぁ遅延評価って?
ここに書いてあることは自信ないので参考程度にしてください。
メソッド起動式の評価に、レシーバとなる一次式の評価と、引数リスト中の式の評価が必要になる、と書きました。
起動するメソッドの中でその引数を使っている、使っていないにかかわらず引数リスト中の式は評価する必要があります。
このような評価戦略を、正格な評価戦略と呼びます。
それに対して、メソッドの中で実際に引数を評価する必要が出てきてから引数の評価を行うような評価戦略を、非正格な評価戦略と呼びます。
さらにその中で、一度評価したものを再び評価しないようなものを遅延評価(lazy evaluation)と呼び、毎回評価するものを遅延評価(delayed evaluation)と呼びます。
どちらも遅延評価となっていて紛らわしいので、lazy の方を怠惰評価と訳する場合もあります。が、この呼び方はあまり広まっていないようです。この 2 つを使い分ける必要がある場合に怠惰評価という言葉を使えばいいでしょう。
正格評価と非正格評価で結果が異なる例を挙げておきます。
int infLoop() { while (true) { } return 0; } void f(int i) { }
このようなプログラムがあった際、f(infLoop()) という関数呼び出しを行った場合、
- 正格評価では f の呼び出し前に infLoop の呼び出しが完了している必要があるため、infLoop を呼び出してしまって無限ループとなる
- 非正格評価では f の定義中で i の評価が必要ない(i の値を使っていない)ため、infLoop を呼び出さずに済み、無限ループにならない
のように、非正格評価の場合、なんと無限ループになりません。
おまけ
今回は Java に限定しましたけど、C# も遅延リストを遅延評価と表現する人多い感じがします(LINQの仕組み&遅延評価の正しい基礎知識とか)。
C# の言語仕様では「評価」そのものに対する節はないのでもにょるんですが、評価という言葉が使われている部分から、評価の対象はやはり式に限定しています。
また、「7.1.1 式の値」を見ると、「最終的に式が"値"を表すことが求められます」とある*7ことと、評価という言葉の使い方から、Java とほぼ同様の意味で「評価」という言葉を使っているようです。
ということで、C# も遅延評価って言うのは避けたいです。
あと Ruby 方面にも言いたい(enumerable_lzによる遅延評価のススメとか)んですが、Ruby は言語仕様がアレでソレなので・・・ねぇ?持ってないんですよね・・・
Ruby で「遅延評価」って表現する場合は、何を評価と言っているかを明らかにしてほしいところです。
遅延評価とは関係ないですが、三項演算子とか参照渡しとかについても、こう・・・まぁいいや。
*1:と、実は続きを読むとそう書いてあります
*2:それに、後で述べますが、メソッド起動式は遅延評価されません
*3:この 2 つを区別するような人はそもそも誤用しないでしょう
*4:Java 8 では Stream と呼ぶらしい。InputStream とかあるのに Stream なんて名前付けちゃうのもいらぬ混乱の元となりそうなんでどうかと思うんですが・・・
*5:Java 言語仕様では calcurate や calcuration という単語が含まれる節はないので、「計算」という言葉に身構えなくて大丈夫です。たぶん。
*6:例を簡単にするために単純化してありますが、実際には Predicate というインターフェイスだし、シグネチャも微妙に違います
*7:この直前に「式が関係する構造のほとんどでは、」のほとんどが何を言っているのかは気になるが・・・