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
String result = m().getOrElse(""); // String result = m().match(s -> s, "");は面倒
String result = m() == null ? x : "";
で、例外を投げる API だった場合は
String result; try { result = m(); } catch (Exception e) { result = ""; }
です。
map
Just の時に、それに包まれた値に対して何かを行うメソッドがすでにある場合に、いちいち match の中でそのメソッドを呼び出し、さらに just で包むのはだるいです。
そこで map ですよ!
例えば id で引っ張ってきた Maybe
そして、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 を返したという情報」です。
しかもこれは実行時ではなく、開発時に手に入る情報ですので、ぬるぽよりも質の高い情報です。
その場合の処理は一緒にそこに書く必要があるのですから。