Optionalは引数に使うべきでない、という幻想について

継続渡しすると戻り値は引数になるから「Optional は戻り値にのみ使うべき」というルールは無意味だよ、という話。 あ、そういう話ね、と分かった方はこれ以上読む必要はありません。

MonoAsync + Optional + 例外という欲張りパック状態なのも問題ですが、それについてはまた今度(Mono<Optional<T>> 使わずに Mono<T> を使え、という指摘があり得る。ただ、そっちもそっちで言いたいことはある、という程度)。 今回は、 MonoAsync くらいの意図として使っています*1

まず、こんなメソッドがあったとします。

Mono<Optional<String>> f();

これ自体は戻り値に Optional を使っているだけなので、「Optional は戻り値にのみ使うべき」は守っています。

しかし、これを使う側はそうはいきません。 例えば、値が取ってこれなかった場合はログして404を返し、取れてきた場合はログして200を返すような処理を考えます。

Mono<ServerResponse> g(ServerRequest req) {
    return f().map(strOpt -> {
        if (strOpt.isEmpty()) {
            logger.info("str is not found.");
            return ServerResponse.notFound();
        }
        var str = strOpt.get();
        logger.info("str is {}.", str);
        return ServerResponse.ok();
    });
}

ここで、map に渡すラムダ式の引数は Optional<String> になります。 おおっと、引数に Optional が出てきてしまいました。

このように、Mono など「後続処理をラムダ式として受け取る」ようなスタイルの設計においては、戻り値を引数として受け取ることになります。

これを「ラムダ式の引数は例外とする」というルールを加えてしまうと、メソッド参照によるラムダ式の置き換えが出来なくなってしまいます。

Mono<ServerResponse> g(ServerRequest req) {
    return f().map(this::h);
}

ServerResponse h(Optional<String> strOpt) {
    if (strOpt.isEmpty()) {
        logger.info("str is not found.");
        return ServerResponse.notFound();
    }
    var str = strOpt.get();
    logger.info("str is {}.", str);
    return ServerResponse.ok();
}

これは先ほどのラムダ式をメソッドとして抽出しただけで、やっていることは同じです。 ラムダ式の中身が複雑になる場合は自然にこういう書き換えはやりたくなるとおもいますが、ラムダ式を例外とするだけでは足りないことが分かります。 まぁ、 public 以外にはルールは適用しない、みたいな例外を追加するようなことは考えられますが、いっそあきらめて Optional を引数に使わない、なんてルールやめてしまった方がいいと思います。

いやいや、複雑になったとしても取得処理を呼び出し側でやって、メソッドの引数には Optional を許さない!という態度もなくはないです。

Mono<ServerResponse> g(ServerRequest req) {
    return f().map(strOpt -> {
        if (strOpt.isEmpty()) {
            logger.info("str is not found.");
            return ServerResponse.notFound();
        }
        var str = strOpt.get();
        logger.info("str is {}.", str);
        return h(str);
    });
}

ServerResponse h(String str) {
    // strを使った複雑な処理
    return ServerResponse.ok();
}

こんな感じですね。 値の有り無しの振り分けをラムダ式側、処理自体をメソッド側と役割を分けられるため、なくはないです。 ただ、今回の例は Optional が1つでしたが、これが2つ、3つと増えてくると、ラムダ式内が複雑になっていくため、そこにテストを書くためにメソッド化したくなります。 その際は、やはり Optional を引数に持つメソッドが欲しくなります。

このように、 「Optional を引数に使わない」というルールは実践的には無意味です。捨てましょう。

「フィールドに Optional を使わない」というルールの方はもうちょっとだけ分からなくもない理由付けがあります*2が、 こちらもすべてのクラスがシリアル化対象なわけでもないので、個人的には無意味だと思っています。

*1:CompletableFutureでもよかったけど長いしCFの語彙が気に入らないので・・・

*2:Optionalがシリアル化可能ではない

re:僕にとってMaybe / Nullable / Optional が、どうしてもしっくりこないわけ。

元ネタ: 僕にとってMaybe / Nullable / Optional が、どうしてもしっくりこないわけ。 - 亀岡的プログラマ日記

OOPの文脈で見ると、元の記事が言っていることもわからなくはないのですが、対象が広すぎていろいろと不正確になってしまっているので、ちょっとまとめてみます。

元の記事が対象にしているのは、Maybe / Optional / Nullableの3つです。 対応する型を持つ言語を見てみると、下記のようになります。

これらは、「値がないこと」を表すもの、という見方では同じですが、それぞれ異なる価値観の元に作られています。

Maybe/OptionalとNullable

これらはすべて型パラメータを取ります*1。 しかし、この中でNullableだけは型パラメータに値型のみという制約が付きます。 これは、C#Nullableは他のものとかなり性質が違うことを意味しています。 元の記事では並べて書かれていましたが、そもそもNullableに関してはここに並べるべきものではありません。

C#のNullable

C#Nullableは、値型にもnullが使いたいという動機で導入された機能です。 そのため、NullableMaybeOptionalと違って、むしろnullを積極的に使えるようにするための機能と言えるでしょう。 これによってC#の上ではNullableを使うことでコード上の見た目だけに関しては、値型でもnullが使えるように見せています。 見た目だけの話であれば、SwiftOptionalと同じように型名の後ろに?が付くため、同じような言語機能に見えますが、実際には全くの別物と考えたほうがいいでしょう。

JavaのOptional

JavaOptionalは、メソッドの戻り値として使うことが想定されているらしく、引数やフィールドに使うことは想定されていないそうです。 JavaOptionalは参照型なので、Optional自体がnullになりうるという点を考えると、引数に使うべきではないというのもある程度納得できます*2

Javaでは現状、型パラメータに指定できるのは参照型だけであり、nullになりうるのも参照型だけです。 そのため、JavaOptionalC#Nullableと違って、(戻り値としての)nullを排除しよう(少なくとも、減らそう)という動機で導入されたとみていいでしょう。

SwiftのOptional

SwiftOptionalは、Javaと名前は同じですが、より言語の仕組みに根差しています。 Javaでは、Optional<T>型の変数にT型の値を代入できません。

Integer i = 42;
Optional<Integer> x = i; // コンパイルエラー

それに対して、Swiftではそれが可能です。

var i: Int = 42
var x: Optional<Int> = i // OK
// Optional<Int>はInt?とも書ける

SwiftではOptionalではない型にnull*3は代入できません。 ですが、Optionalという仕組みによって今までの知識からあまり外れない書き方でnullを扱えるようになっています。

HaskellのMaybe

HaskellMaybeは実装としてはSwiftOptionalに一番近くなっています。 ただ、他の言語と違ってHaskellではそもそも他の言語のようなnullはなく、Swiftのように既存言語ユーザーを取り込むために「今までの知識からあまり外れない書き方」というのも求めていません。 結果だけ見ると、JavaOptionalと同じように、Maybe t型の変数にt型の値は代入できません。

Nullのダメな理由

元記事では、「Nullは何がダメなんだっけ?」について、

  1. Nullは型ではない
  2. あるメソッドがNullになるかどうかの判断ができない

が挙げられています。 細かい点ですが、ここにも誤解があります。

(少なくともJavaでは)Nullは型を持つ

まず、Javaの話であればnullは型を持っています*4。 そのため、「ClassHogenull」という表現は、少なくともJavaにおいては正確ではなく、正確には「ClassHoge型に暗黙変換されたnull」とでもなるでしょうか。 詳しくは言語仕様を参照してください。

ここで言いたかったのはおそらく、「nullはどんな型にも変換できてしまう」ということでしょう。

あるメソッドの結果(もしくは変数)がNullかどうかの判断が型だけからできない

これも細かいですが、あるメソッド(の結果)がNullになるかどうかの判断ができないというのは、正確には「型を見ただけでは」できないのであり、元記事中にあるように、if文によって実行時にはチェックできます。

本題

さて、ここからが本題です。 元記事では、Optionalが解決するのは2だけ、としていますが・・・

Javaの場合

JavaOptionalでは、Optional<T>型とT型は全くの別物であり、相互に暗黙変換できません。 そのため、

  1. (nullに相当する)Optional.empty()Optional型の変数にしか入れることはできない*5
  2. あるメソッドの結果がOptional.empty()になりうるかどうか型だけで判断できる

となります。

Swiftの場合

SwiftOptionalは、Optional<T>型の変数にT型の値を代入できますが、その逆はできません。 そのため、

  1. (nullに相当する)nilOptional型の変数にしか入れることはできない
  2. あるメソッドの結果がnilになりうるかどうか型だけで判断できる

となります。

Haskellの場合

HaskellMaybeは、Maybe t型とt型は全くの別物であり、相互に暗黙変換できません。 そのため、

  1. (nullに相当する)NothingMaybe型の変数にしか入れることはできない
  2. ある関数の結果がNothinsになりうるかどうか型だけで判断できる

となります。

元記事が言いたかったこと

このように、OptionalもしくはMaybeは、1も2も解決できています。 これは十分なメリットであり、このメリットだけでもOptional/Maybeは有用です。

ただ、元記事を見ると、

本当に解決したいのは「Nullに型独自の処理をさせたい」なので、残ったままです

とあります。 つまり、本当に問題にしていたのは、「型Aの値がない場合と型Bの値がない場合で別の処理をさせたい」となります。

if文相当の処理を書く必要性

Optional/Maybeを使ったコードで頻繁にif相当の処理を書いてしまっている場合、それはそもそもOptional/Maybeの使い方を間違っている可能性が大きいです。 リストを操作する関数にリストを返す関数が多く含まれるのと同じように、あるいはTaskを返す関数を内部で呼び出している関数がTaskを返す関数になるのと同じように、Optional/Maybeを返す関数を内部で呼び出している関数は、その関数の戻り値もOptionalになることがよくあります。 この場合、if相当の処理を書くのは間違っています。

// Javaだと思う(Swiftは書いたことないので)
Optional<User> user = findUser(pred);
if (user.isPresent()) {
    // 何かする
    return Optional.of(何か処理の結果);
} else {
    return Optional.empty();
}

例えば、「何かする」の部分がuserの中身を使った関数を呼び出すような処理の場合、

// 値がなかった場合のことは気にしない
return findUser(pred).map(user -> 何かする関数(user)); // ラムダ式を使わずに直接関数を渡す、でも可

のようになるでしょう。 mapの他にも、様々なメソッドが用意されているため、ifによる分岐に頼る場面は少なくなります。 Haskellの場合、do構文によってさらに複雑な例でもすっきり書けますが、それはまた別の話。

手続き的には見えない

さて、上のコードが手続き的に見えるでしょうか? 呼び出し側はOptionalのチェックを行っておらず、コードも短く簡潔です。

ではオブジェクト指向プログラミングではどのようになるでしょうか? Userに対してNullObjectを用意して、このようになるでしょう。

public interface User {
    戻り値の型 何かする処理();
}
public class UserImpl implements User {
    @Override
    public 戻り値の型 何かする処理() {
        // 何かする
        return 何かした結果;
    }
    // ...
}
public class EmptyUser implements User {
    @Override
    public 戻り値の型 何かする処理() {
        return 戻り値の型のNullObject;
    }
    // ...
}
// 呼び出し側は意識しない(意識できない)
return findUser(pred).何かする処理();

呼び出し側だけ見ると、Optionalのときとそんなに変わってませんよね*6。 つまり、元のコードも手続き的には見えないと言っていいでしょう。

NullObjectをちゃんと使いたい、というよりは、Optionalをどう使うか考えたい

OOPの文脈で、どこまでOptionalを使えばいいのかというのはよくわかりません。 Optionalを使うと、どうしてもコードはOOPから離れて行ってしまうという感覚は確かにあります。 どのあたりでバランスを取るのがいいのかはケースバイケースでしょうし、一般化できるものではないと思っています。

コメント欄では id:kyon_mm が(やはりOOPの文脈で)

Optionを返していいのはむしろprivateやprotected的なものだけだと思っていて、それ以外はオブジェクトだったり、バリアント(判別共用体)的な感じで返すのが「オブジェクトとやり取りをしていて、それぞれに責務が割あたっている」といえるのではないだろうか。と思っています。

と言っています。 これはこれで一つの態度ではありますが、基本的なライブラリでは Optional を返す関数*7というのはもっと積極的に作られることになると思います。

FPに軸足を置くか、OOPに軸足を置くかは、対象の性質やメンバーのスキル等によって決めていくしかないでしょう。 ただし、間違ったOptionalの使い方をもって「しっくりこない」というのであれば、まずはOptionalの正しい使い方を学び、実践すべきです。 そのための言語としては、F#なんていかがでしょうか。

*1:Javaは取らなくても許されるとかいう細かい話は置いといて。

*2:フィールドに使うべきではない理由はよくわかりませんが

*3:Swiftではnilだが、この記事ではnullで統一

*4:ただし、その型の変数やフィールドを作ることはできない

*5:Objectは無視します

*6:findUserがnullを返す可能性は考慮しなくていいのかという点はここでは考慮しない

*7:もしくは、Optionalを返す関数を受け取る関数

2つのOptionalで分岐する

僕のOptional<>の使い方がカッコワルイ。 - 谷本 心 in せろ部屋にある最後の例ですが、 2つのOptionalの組み合わせによって処理を分岐させるコードが幾つか載っています。 ifで書いた例を見てみましょう。

public String hello(Optional<String> name1, Optional<String> name2) {
    if (name1.isPresent()) {
        if (name2.isPresent()) {
            return call(name1.get(), name2.get());
        } else {
            return call1(name1.get());
        }
    } else {
        if (name2.isPresent()) {
            return call2(name2.get());
        } else {
            return call();
        }
    }
}

まとめると、こんな感じでしょうか。

name1 name2 呼び出すメソッド
値有り 値有り 2引数call
値有り Empty call1
Empty 値有り call2
Empty Empty 0引数call

これ、F#だったらmatch式を使えば分かりやすく書けます。

let hello name1 name2 =
  match name1, name2 with
  | Some name1, Some name2 -> call(name1, name2)
  | Some name1, None -> call1(name1)
  | None, Some name2 -> call2(name2)
  | None, None -> call()

JavaOptionalにもしmatchメソッドが実装されていれば、こんな感じに書けるんですけどね。

public String hello(Optional<String> name1, Optional<String> name2) {
    return name1.match(
        n1 -> name2.match(n2 -> call(n1, n2), () -> call1(n1)),
        () -> name2.match(n2 -> call2(n2), () -> call())
    );
}

Optionalのような型を導入するのであれば、やはりmatch式のようなもの*1が欲しくなりますね、という話でした。

*1:あとついでにモナド用の構文も・・・

なごやかJavaで発表してきた

なごやかJava第一回で、「.NET系開発者から見たJava」というタイトルで発表してきました。 Javaのこの機能って.NET開発者から見てどうなの?というような内容です。

大阪から参加してくれた方の感想を載せておきます。

JavaでTupleってみる

可変長な型変数の表現より、タプルの話です。

.NETのタプル

.NETでは、型引数の数が違うクラスを定義できるので、Tupleという名前で7要素まで対応しています。

public class Tuple<T1> { ... }
public class Tuple<T1, T2> { ... }
public class Tuple<T1, T2, T3> { ... }
public class Tuple<T1, T2, T3, T4> { ... }
public class Tuple<T1, T2, T3, T4, T5> { ... }
public class Tuple<T1, T2, T3, T4, T5, T6> { ... }
public class Tuple<T1, T2, T3, T4, T5, T6, T7> { ... }

そして、各要素にアクセスするために、Item1, Item2のようなプロパティを使います。

さて、では.NETのタプルは8要素以上は対応していないかと言うと、そうではありません。 タプルのインスタンスを作るためには、Tuple.Createというメソッドを使って作るのが楽です。 そしてこのTuple.Createは、なんと8引数版まで用意されているのです。

var t = Tuple.Create(true, 2, 3.0, "4", new[] { 5 }, 6M, 7L, 8);

このtの型は、こうなります。

Tuple<bool, int, double, string, int[], decimal, long, Tuple<int>>

実は、Tupleは8番目の型引数として、「残りの要素を保持するタプルの型」が受け取れるようになっています。 ちなみにTuple<T1>という謎のクラスはこの時のみ使われるクラスです。

public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest> { ... }

「残りの要素を保持するタプル」にアクセスするためには、Item8ではなく、Restという名前のプロパティを使います。 そのため、8要素タプルの8番目の要素にアクセスしたい場合は、

var value = t.Rest.Item1;

となります。 さてさて、ここからが.NETの残念なところなのですが、実は9要素以上のタプルを作る簡単な方法は用意されていません*1。 ので、コンストラクタを使うことになります。

var t = new Tuple<int, int, int, int, int, int, int, Tuple<int, int>>(
    1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9));

コンストラクタの型パラメータは省略できないので、悲惨なことになっていますね・・・ これは、Tuple.Createの8要素版が8要素タプルを作るためではなく、ネストしたタプルを作るためのヘルパーになっていればもっと楽が出来たのです。

var t = Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9)); // こうはなっていない

残念すぎる・・・

Restという考え方

さて、.NETのタプルは残念ですが、「足りない部分はRestで」というのはどこかで聞いたことのあるような話です。 そう、コンスセルですね!

ということで、コンスセルの考え方を使ってタプルのないJavaにタプルを作ってみましょう。 まずは、コンスセルの終端を表すためのクラスを導入します。 以降では、特にパッケージとか書きませんけど、全部同一パッケージに入っていると思ってください。

public final class TupleNil {
    static final TupleNil nil = new TupleNil();
    private TupleNil() {}
}

簡単ですね。 こいつは状態を持っておらず、インスタンスも外部からは生成できず、 さらには唯一の静的フィールドであるnilもパッケージプライベートなので、 パッケージ外からはこのフィールドにすらアクセスできません。 これだけでは本当に全く何の役にも立たないクラスです。

次に、2つの型パラメータを取って実際に要素を保持するクラスを作ります。

public final class TupleCons<T, Rest> {
    public final T value;
    public final Rest rest;

    TupleCons(T value, Rest rest) {
        this.value = value;
        this.rest = rest;
    }
}

これも簡単ですね。 単に、値を2つ持っているだけのクラスです。 このクラスのvalueに値が、restに残りの要素を表すものが格納されます。

コンストラクタがパッケージプライベートなので、パッケージ外からこのクラスのインスタンスを生成することはできません。

最後にタプルを作るためのメソッドを持ったクラスです。

public final class Tuple {
    private Tuple() {}

    public static <T> TupleCons<T, TupleNil> singleton(T value) {
        // こことか
        return new TupleCons<T, TupleNil>(value, TupleNil.nil);
    }

    public static <T1, T2, Rest> TupleCons<T1, TupleCons<T2, Rest>> cons(T1 value, TupleCons<T2, TupleNil> rest) {
        // ここってダイアモンド演算子使えるんですか?使えそうだけどJavaとかわかりません><
        return new TupleCons<T1, TupleCons<T2, Rest>>(value, rest);
    }
}

あとは、これを使ったコードです。

TupleCons<Integer, TupleCons<String, TupleNil>> t2 = Tuple.cons(42, Tuple.singleton("hoge"));
System.out.println(t2.value); // 42
System.out.println(t2.rest.value); // hoge

やったー!(何が

まとめ(ない)

  • .NETのタプルは割と現実見て、7要素までは自然に扱える
    • 大量要素のタプル使うな
    • でももし扱う場合があるといけないから、一応対応しとくぜ!
  • .NETのTuple.Createはクソ
    • 8要素版をなぜそういう形で用意したし
    • 9要素以上のタプルを作りたい場合はコンストラクタで頑張るしかない
    • それLangExtならCreate.Tuple(1, 2, 3, 4, 5, 6, 7, 8, 9)でできるよ!
    • それF#なら(1, 2, 3, 4, 5, 6, 7, 8, 9)でできるよ!
  • タプルってコンパイル時のリスト(コンスセル)だよね!
    • Javaでそういう実装してみた
    • 元記事が後ろに後ろに拡張していくのに対して、こっちは(consで)前に前に拡張していくという違いがちょっと面白い
    • 元記事がインスタンスメソッドで拡張していくのに対して、こっちはクラスメソッドで拡張していくのもちょっと面白い
      • インスタンスメソッドにもできるけど、キモイことになる(字面上は後ろに書いたものが前に追加される)のでやめた
    • 実用性?しりませんなぁ

*1:まぁ、そんなタプル作るなと言われればその通り、としか言えないですが

C# 使いから見てうらやましい Java8 の default 実装の使い方

Java8 から追加されるインターフェイスの default 実装ですが、C# の拡張メソッドに似てますよね。 実際、このどちらも「シンインターフェイス」を定義するだけで「リッチインターフェイス」が手に入ります。

しかし、C# の拡張メソッドと Java のインターフェイスの default 実装には、それぞれの利点と欠点があります。

拡張メソッドの利点

拡張メソッドの利点は、インターフェイスの実装者だけでなく、 インターフェイスの使用者に対してもインターフェイスの拡張が開かれている点です。 既存の型ですら、後付けでメソッドを追加することができるということです。

using System;

public static class StringExtension
{
    // インターフェイスでなくても、どんな型に対しても拡張可能
    public static int ToInt(this string self)
    {
        return int.Parse(self);
    }
}
var num = "100".ToInt();

それに対して Java8 のインターフェイスの default 実装は、 あくまでインターフェイスの提供者しか提供できません。

これは、Java8 の default 実装に対する、拡張メソッドの大きなメリットです。

default 実装の利点

では default 実装には利点がないか、というと、そうではありません。 default 実装は、リッチインターフェイスをシンインターフェイスに変換してしまうこともできるのです。

import java.util.*;

// Appendableは3つのメソッドを提供するインターフェイスだが、
// SamAppendable(SamはSingle Abstract Method)はそのうち2つをdefault実装として提供することで、
// 1つの抽象メソッドを実装すればいいようにしている
interface SamAppendable extends Appendable {
    Appendable append(CharSequence csq, int start, int end);
    default Appendable append(CharSequence csq) {
        this.append(csq, 0, csq == null ? 0 : csq.length());
        return this;
    }
    default Appendable append(char c) {
        this.append("" + c, 0, 1);
        return this;
    }
}

public class Main {
    static void appendHoge(SamAppendable a) {
        a.append("hoge");
    }
    public static void main(String[] args) {
        // (実はあまりいい例ではないのだけれど)
        // SAMインターフェイスにしたおかげで、ラムダ式が使える!
        appendHoge((csq, start, end) -> {
            System.out.printf("csq: %s, start: %d, end: %d%n", csq, start, end);
            return null;
        });
    }
}

それに対して、拡張メソッドではこのようなことは出来ません。

ジェネリックな IEnumerable は非ジェネリックな IEnumerable を継承しているため、 GetEnumerator を 2 種類実装する必要があって面倒なんですが、 「あれ、Java 8 の default 実装だとこの問題解決できるんじゃ?」 ・・・と思ったのがこのエントリを書くことになったきっかけです。 なるほど、拡張メソッドにしなかったのには理由がありそうですね。

そんなことより Scala ですよ

こんな感じで、どちらも一長一短であって優劣つけがたい感じなのですが・・・ なんと、Scala の trait はこのどちらにも対応しています。

既存の型へのメンバの追加

object Extensions {
  implicit class StringExtension(val value: String) {
    def toInteger: Int = value.toInt
  }
}

2.10 からかなり簡単に書けるようになりました。

リッチインターフェイスのシンインターフェイス化

trait SamAppendable extends Appendable {
  def append(csq: CharSequence, start: Int, end: Int): Appendable
  def append(csq: CharSequence): Appendable = {
    this.append(csq, 0, if (csq == null) 0 else csq.length)
    this
  }
  def append(c: Char): Appendable = {
    this.append("" + c, 0, 1)
    this
  }
}

Scala では Appendable を SAM インターフェイスにする意味はないですが、 リッチでファットなインターフェイスをシンインターフェイスにできるのは、 既存の Java コードとのやり取りを考えるとありがたいかもしれません。

Java8

Java8 に期待するより、今使える Scala 使おう! でも、default 実装が拡張メソッドの劣化コピーとか言っちゃダメ!

遅延評価いうなキャンペーンとかどうか

遅延評価については以前も書いてるんですが、そのときは結論なしでした。
が、ちょっと考えるところがあって、言語を 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 での評価というのは「式」から「値」を生み出す(もしくは式を値に変換する)ことを言うと思っていいでしょう。
評価の結果が変数となっても、それを囲むより大きな式を評価する際には、その変数に格納されている値が必要であることが多いからです。
「無し」は今回の本筋とはあまり関係がないので、無視します。


重要なのは、Java においては式のみが評価の対象となる、ということです。
文は評価(evaluation)ではなく、実行(execution)と呼ぶようですね。

「(遅延)評価」の誤用の例

このエントリを書くきっかけとなったものですが、

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、変数名は stream2、式は stream.filter(n -> n%2 == 0) です。

メソッド起動式

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、引数リストはなし、クラス本体は { public boolean apply(Integer n) { return n%2 == 0; } } です。


何をしたかったかというと、メソッド起動式を評価するために、メソッドの引数リストであるラムダ式を評価して値にすることでした。
ここで、ラムダ式を値にすると何になるかはもう明白ですね。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:この直前に「式が関係する構造のほとんどでは、」のほとんどが何を言っているのかは気になるが・・・