Maybe のエントリの補足

昨日書いた

Java の語彙で Maybe を説明してみる - ぐるぐる〜

に予想以上の反響があってびっくりです。
色々反応もらったので、ちょっと補足を。上のエントリを読んでない人は読んでからどうぞ。

@CheckForNull でいいのでは?

はい、確かに FindBugs の CheckForNull アノテーションは便利です。
でも、これが提供してくれるのは null チェックの強制です。
先のエントリは、「null より安全な Maybe」という説明でした。
ですので、それだけを達成するのであれば CheckForNull アノテーションでもいい*1のですが、後半でちょっと見たように「null より便利な Maybe」という側面もあります。
先のエントリでは bind と or だけ追加しましたが、他にも色々と追加してみましょう。

// Java8だよ!
package maybe;
import java.util.functions.*;
import static functions.FunctionModule.*;

public final class MaybeModule {
    private MaybeModule() {}
    // 略
    public interface Maybe<E> {
        <R> R match(Func1<E, R> ifJust, Func1<Unit, R> ifNothing);
        // bindとorは略

        // Justなら包まれた値を、Nothingなら引数で指定した値を返す
        E getOrElse(E ifNothing) default {
            return match(v -> v, _ -> ifNothing);
        }

        // Justなら渡された関数を実行してJustで包んで返し、Nothingなら何もせずにNothingを返す
        // ※Mapperはjava.util.functionsで提供されるらしい
        <R> Maybe<R> map(Mapper<E, R> f) default {
            return match(v -> just(f.map(v)), _ -> nothing());
        }

        // Justなら渡された処理を実行し、Nothingなら何もしない
        // ※Blockはjava.util.functionsで提供されるらしい
        void iter(Block<E> b) default {
            match(v -> b.apply(v), _ -> {});
        }

        // Justなら渡された真偽値を返す関数を実行して返し、Nothingなら何もせずにfalseを返す
        // ※Predicateはjava.util.functionsで提供されるらしい
        boolean exists(Predicate<E> p) default {
            return match(v -> pred.test(v), _ -> false);
        }

    }
    // 略
}

以下のメソッドを追加してみました。

  • getOrElse
  • map
  • iter
  • exists
getOrElse

Nothing だった場合のデフォルト値がある場合に、いちいち match を書くのはだるいです。
そこで getOrElse ですよ!
例えば Maybe を返すメソッド m があったとして、結果の中身が取り出したいけど Nothing なら空文字列でいい、という場合、

String result = m().getOrElse("");
// String result = m().match(s -> s, "");は面倒

です。
null を返す API の場合は条件演算子で、

String result = m() == null ? x : "";

で、例外を投げる API だった場合は

String result;
try {
    result = m();
} catch (Exception e) {
    result = "";
}

です。

map

Just の時に、それに包まれた値に対して何かを行うメソッドがすでにある場合に、いちいち match の中でそのメソッドを呼び出し、さらに just で包むのはだるいです。
そこで map ですよ!
例えば id で引っ張ってきた Maybe に対して、もし取れてきてたら name の空白以前を取得したい場合を考えます。
そして、User の name から空白以前を取得するメソッドは既にあるので、それを使いたいという状況です。

class Hoge {
    // Userは先のエントリのアレ + idも持ってる感じで
    // Usersはstaticにした感じで
    public static String firstName(User u) {
        return u.name.split(" ")[0];
    }
}
public static Maybe<String> foo(int id) {
    return Users.findById(id).map(Hoge::firstName);
    // return Users.findById(id).match(u -> just(Hoge.firstName(u)), _ -> nothing());は面倒
}

例外を投げる API だった場合は、

public static String foo(int id) {
    return Hoge.firstName(Users.findById(id));
}

どっかで例外は処理してください。
null を返す API だった場合は、

public static String foo(int id) {
    User u = Users.findById(id);
    return u == null ? null
                     : Hoge.firstName(u);
}

です*2


また、map は bind のチェーンの最後に使うと Just で包む必要がなくなるので、ちょっとうれしいです。
例えば、

static Maybe<Integer> m1() { /* 略 */ }
static Maybe<String> m2() { /* 略 */ }
static Maybe<User> m3() { /* 略 */ }
static Maybe<Boolean> m4() { /* 略 */ }
static Something hoge(int i, String s, User u, boolean b) { /* 略 */ }

これを m1 から順番に m4 まで実行し、全部取れたら hoge に渡したかった場合、bind だけだと

m1().bind(
  i -> m2().bind(
  s -> m3().bind(
  u -> m4().bind(
  b -> just(hoge(i, s, u, b))))));

こんな感じです。
これを map を使うことで、

m1().bind(
  i -> m2().bind(
  s -> m3().bind(
  u -> m4().map(
  b -> hoge(i, s, u, b)))));

と、最後 Just で包む必要がなくなり微妙にうれしいです。
え、わからない?書いてて自分もあんま嬉しくないかも、とか思っちゃいました。微妙。
同じようなことを null を返す API でやる場合は面倒なので書きません。考えてみてください。
例外を返す API だった場合は素直に実装できます。こちらも考えてみてください。

iter

iter は map の戻り値に興味がない場合に便利です。
map だと、値を何か返す必要があるため、

// procは何も返さない関数
x.map(x -> { proc(x); return Unit._; });

と、面倒です。iter を使うと、

x.iter(x -> proc(x));

でよくなり、proc が Hoge クラスに定義されている場合は更に、

x.iter(Hoge::proc);

と書けます。

exists

返ってきた値によらない何らかの処理 proc を実行する判断を、返ってきた値に対する何かでする場合、match を書くのはだるいです。
そこで exists ですよ!

if (Users.findById(id).exists(u -> !u.name.contains(" "))) {
    proc();
}
/* これはだるい
Users.findById(id).matchAndAct(
    u -> { if (!u.name.contains(" ")) proc(); },
    _ -> {}); */

null を返す API だった場合は、

User u = Users.findById(id);
if (u != null && !u.name.contains(" ")) {
    proc();
}

一次変数を外に書かなきゃいけないのが残念です。
例外を投げる API だった場合は、

try {
    if (Users.findById(id).name.contains(" ")) {
        proc();
    }
} catch (Exception e) { /* do nothing */ }

うん。

Maybe 自体が null になるのは防げないのでは?

はい。あくまで出発地点は「User を安全に扱いたい」でしたので、それをラップしてる Maybe は null にできてしまいます。
なので、Maybe を仮に導入するのだとしたら、開発者全員が Maybe がなぜ存在するのかを理解する必要があります。
それを理解していたら、Maybe に null を入れる、という状況は「うっかりミス」くらいでしょう*3


そんなに開発者のレベル高くないよ><。
という場合、Java での Maybe は正直役に立たないでしょう。
「ドキュメンテーションコメントをちゃんと書かないプログラマは普通にいる」と同じレベルで、「Maybe に null を入れてしまうプログラマは普通にいる」となるだけです。


悪あがきの方向としては、FindBugs の NonNull アノテーションとセットで覚えてもらう、とか?
「@NonNull Maybe という特殊な書き方があってだね・・・」みたいな導入。無理か・・・

例外を握りつぶすような人は Nothing の場合の処理を空実装にするのでは?

match と一緒に、getOrElse を教えましょう*4
そうすれば、Nothing の場合に空に相当するもの (null だとか空文字列だとか -1 だとか) を返すラムダ式を書くより、getOrElse にそれを渡した方が楽ですので、そちらを使うはずです。
つまり、こういう住み分けになることを期待するのです。

  • match を使っている場所は少なくとも Nothing の場合をある程度は考慮している
  • getOrElse を使っている場所は危険なので重点的にレビュー。特に getOrElse(null)

また、例外処理は文なのである程度なんでもできますが、Maybe は式となるので型を合わせるという作業が必要です。
そのため、例外処理を握りつぶすよりは「Nothing の場合どうなるべきか」を考えなければならないはずです。
それが嫌な (もしくはできない) 人はたぶん getOrElse(null) とか使います。

コーディング量が増え、効率も落ち、失敗時に原因が分からないならぬるぽの方がマシでは?

コーディング量は Java8 を待てば割と改善されます。
効率は誰か計測してください。
この 2 つは分からなくもないです。
が、「失敗時に原因が分からないからぬるぽの方がマシでは?」というのは違います。


Maybe の問題点として挙げた「失敗時に原因が分からない」といった場合の「失敗」とは、バグのことではありません。
対して、ぬるぽはバグです。
ぬるぽで落ちたときにいくらスタックトレースが出ようが、それから得られる情報は「バグを探すための情報」です。
決して、「失敗の原因となった情報」ではありません。
そこをあえて「何かが null だったという情報」というのであれば、その程度の情報なら Nothing も持っています。
「何かが Nothing を返したという情報」です。
しかもこれは実行時ではなく、開発時に手に入る情報ですので、ぬるぽよりも質の高い情報です。
その場合の処理は一緒にそこに書く必要があるのですから。

*1:駄目かもしれないけど別の視点から答えるのでいいことにしておく

*2:firstName に null チェックを入れる、でもいいです

*3:return null はうっかりやってしまいそうです

*4:現行の Java の場合、default 実装が使えないので Just クラスと Nothing クラスで実装を与えることになります