null の扱い
Maybe つながりで、色んな言語の null の扱いについてちょっとだけまとめてみた。
観点は一つ、「this が null の場合に this の指す先にアクセスしないメソッドが呼び出せるか」です。
C++
#include <iostream> struct hoge { void f() { if (this == 0) std::cout << "this is null." << std::endl; else std::cout << "this is not null." << std::endl; } }; int main() { hoge* h = 0; h->f(); // => this is null. }
できます。
追記:
ideone で試してできただけで、未定義動作でした。(thx: id:uskz)
参考:API Only - Stack Exchange
Java
public class Main { void f() { if (this == null) System.out.println("this is null."); else System.out.println("this is not null."); } public static void main(String[] args) { Main x = null; x.f(); // NullPointerException } }
できません。
C#
class Program { void F() { if (this == null) System.Console.WriteLine("this is null."); else System.Console.WriteLine("this is not null."); } public static void Main() { var x = default(Program); x.F(); // NullReferenceException } }
できません。が、拡張メソッドを使うと見た目上はできるように書けます。
class Program { public static void Main() { var x = default(Program); x.F(); // NullReferenceException } } static class ProgramExt { public static void f(this Program self) { if (self == null) System.Console.WriteLine("this is null."); else System.Console.WriteLine("this is not null."); } }
F#
[<AllowNullLiteral>] type Hoge() = member x.f() = if x = null then printfn "this is null." else printfn "this is not null." let x: Hoge = null x.f() (* => this is null. *)
できます。null リテラルを直接使いたい場合、AllowNullLiteral 属性を付けなければならない点に注意してください。
JSX
class _Main { function f(): void { if (this == null) log "this is null."; else log "this is not null."; } function main(args: string[]): void { var x: _Main = null; x.f(); // Type Error: Cannot call method 'x$' of null } }
できません。
まとめ?
レシーバをメソッドの隠れた引数と考えるのであればできるし、そうじゃないナニモノかであると考えるのであればできない、ということでしょうか。
レシーバが null とはどういうことだ!けしからん!!とかってなって即ぬるぽ。
これができることによる問題って思いつかないんだけど、何かあるんだろうか。
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 を返したという情報」です。
しかもこれは実行時ではなく、開発時に手に入る情報ですので、ぬるぽよりも質の高い情報です。
その場合の処理は一緒にそこに書く必要があるのですから。
Java の語彙で Maybe を説明してみる
java-jaで例外処理の話をしてきました - 西尾泰和のはてなダイアリー
を読んで。
Maybe は値があるかないかを型で表すことができます!そう、直和型なんです!とか言われてもイミフだと思うのです(リンク先のエントリがそう説明してるわけではないですが)。
Java の語彙で Maybe の説明をできたら嬉しい人もいるんじゃないかなぁ、とかなんとか。
ただし、書いてたら結構長くなりました。時間がある人はどうぞ。
Maybe? null より安全に「値がないこと」が扱えるものだよ
スタート地点としてはこれでいいでしょう。
以降で、「なんで安全なの?」という全うな疑問に答えてみたいと思います。
問題点
int で説明すると煙に巻いてしまうような気がしたので、User クラスを見てみます。
import java.util.*; class User { final String name; final int age; User(String name, int age) { this.name = name == null ? "" : name; this.age = age; } } public class Users { private final List<User> users = new ArrayList<User>(); public Users(User...users) { this.users.addAll(Arrays.asList(users)); } // 問題となるメソッド public User findByName(String name) { for (User u : this.users) { if (u.name.equals(name)) return u; } return null; } // サンプル用mainメソッド public static void main(String[] args) { Users us = new Users(new User("hoge", 10), new User("piyo", 20)); User u = us.findByName("foo"); if (u != null) { System.out.printf("name: %s, age: %d%n", u.name, u.age); } } }
main メソッドで null チェックを忘れると、NullPointerException で落ちます。
これを指して「null は安全ではない」と言っています。
こんな小さな例ならまだしも、普通はもっと離れた場所で呼び出しますよね。
このときの態度として、Java プログラマとしては
- ドキュメンテーションコメントに書くべき!
- 例外を使うべき!
- Null Object パターンだ!
あたりを思い浮かべるのではないでしょうか。
ただ、現実を見てみると・・・
- ドキュメンテーションコメント書かないプログラマは普通にいる
- ドキュメンテーションコメント読まないプログラマもいる
- 例外握りつぶすプログラマは普通にいる
- 今回の問題は単純な Null Object パターンできれいに解決できない
最初 3 つは (お察しください) なのでどうしよう・・・規約で縛るしか!でも規約は無視されるものだし、機械的に強制するとコードにノイズが増えてしまいがちです*1。
最後の問題はこういうことです。
import java.util.*; class User { static final User NULL_USER = new User("", -1); // Userを継承して・・・とか面倒なのでこれで final String name; final int age; User(String name, int age) { this.name = name == null ? "" : name; this.age = age; } } public class Users { private final List<User> users = new ArrayList<User>(); public Users(User...users) { this.users.addAll(Arrays.asList(users)); } public User findByName(String name) { for (User u : this.users) { if (u.name.equals(name)) return u; } // nullの代わりとなるオブジェクトを返す return User.NULL_USER; } // サンプル用mainメソッド public static void main(String[] args) { Users us = new Users(new User("hoge", 10), new User("piyo", 20)); User u = us.findByName("foo"); // nullチェックは不要! System.out.printf("name: %s, age: %d%n", u.name, u.age); } }
この変更で findByName は null を返すことはなくなりました。めでたしめでたし。
・・・って、違いますよ!動き変わっちゃってます!
最初の例では、見つからなければ何も出力されませんでした。が、この例では「"name: , age: -1"」って出力されちゃいます。
null チェックしなくとも例外で落ちることはなくなりましたけど、これでは駄目ですね。
かといって if (u != User.NULL_USER) ... と書くのであれば、それ null チェック入れるのと何が違うのさ・・・という。
もうちょっと Null Object パターン
いやいや、さっきのは API がダメだったんだ!
ということで、より柔軟な API を目指してみます。
import java.util.*; class User { interface Case { // NULL_USERの場合に呼び出される処理 void nullCase(); // NULL_USERではない場合に呼び出される処理 // uにはnullではないUserが入ってくる void notNullCase(User u); } static final User NULL_USER = new User("", -1); final String name; final int age; User(String name, int age) { this.name = name == null ? "" : name; this.age = age; } void proc(Case c) { if (this == NULL_USER) c.nullCase(); else c.notNullCase(this); } } public class Users { private final List<User> users = new ArrayList<User>(); public Users(User...users) { this.users.addAll(Arrays.asList(users)); } public User findByName(String name) { for (User u : this.users) { if (u.name.equals(name)) return u; } return User.NULL_USER; } // サンプル用mainメソッド public static void main(String[] args) { Users us = new Users(new User("hoge", 10), new User("piyo", 20)); User u = us.findByName("foo"); // nullチェック不要! u.proc(new User.Case() { public void notNullCase(User u) { System.out.printf("name: %s, age: %d%n", u.name, u.age); } public void nullCase() { /* do nothing */ } }); } }
え、null チェック不要だけど書く量増えてるしこれに何の意味があるの・・・
これが嬉しいのは、「値がないときの処理を書くことを (だいたい) 強制できる」点です。
でも、findByName から返ってくる User オブジェクトをそのまま使ってしまえるため、正直意味ないです。
だいたい強制できる、ではなく強制したい!のです。
あとちょっとだけ Null Object パターン
値がないときの処理を書くことを強制してしまいましょう。
import java.util.*; class User { final String name; final int age; User(String name, int age) { this.name = name == null ? "" : name; this.age = age; } } // このクラスを用意するのがミソ class ResultUser { private final User result; ResultUser(User result) { this.result = result; } interface Case { // ResultUserのresultがnullの場合に呼び出される処理 void nullCase(); // ResultUserのresultがnullではない場合に呼び出される処理 // uにはnullではないUserが入ってくる void notNullCase(User u); } void proc(Case c) { // this.resultの中身に応じて、呼び出すメソッドを振り分ける if (this.result == null) c.nullCase(); else c.notNullCase(this.result); } } public class Users { private final List<User> users = new ArrayList<User>(); public Users(User...users) { this.users.addAll(Arrays.asList(users)); } // ResultUserで包んで返す public ResultUser findByName(String name) { for (User u : this.users) { if (u.name.equals(name)) return new ResultUser(u); } return new ResultUser(null); } // サンプル用mainメソッド public static void main(String[] args) { Users us = new Users(new User("hoge", 10), new User("piyo", 20)); // ResultUserはUserではないので、直接Userとして使うことはできない ResultUser r = us.findByName("foo"); // かならずprocを使って、「値が無い場合の処理」を書くことを強制! r.proc(new ResultUser.Case() { public void notNullCase(User u) { System.out.printf("name: %s, age: %d%n", u.name, u.age); } public void nullCase() { /* do nothing */ } }); } }
これでどうだ!
このバージョンでは、findByName から返ってくるのは User ではなく ResultUser です。
ResultUser から実際の User を使うためには、proc メソッドを使い、Case インターフェイスの notNullCase メソッドの引数として渡してもらうしかありません。
これで、User に関して null の問題で頭を悩ませる必要はなくなりました*2。
長くて面倒だ、と感じるかもしれません。が、安心が手に入る代償だと思えば安いものだと・・・あ、無理ですか。
これに関してはまた後程述べますので、今はスルーしておいてください(解決策があるというわけではないです)。
ResultUser をもっと汎用的にしてみる
null を避けたいクラスに対して、いちいち Result なんとかを作るのは DRY に反します。
ので、これをもっと汎用的にしてみましょう。
色んなクラスで使いたいので、ジェネリクスを使います。
public final class Result<E> { private final E result; public Result(E result) { this.result = result; } public interface Case<E, R> { R nullCase(); R notNullCase(E result); } public <R> R proc(Case<E, R> c) { if (this.result == null) return c.nullCase(); return c.notNullCase(this.result); } }
この Result クラスを使ってさっきの User の例を書き直してみます。
import java.util.*; class User { /* 略 */ } public class Users { private final List<User> users = new ArrayList<User>(); public Users(User...users) { this.users.addAll(Arrays.asList(users)); } public Result<User> findByName(String name) { for (User u : this.users) { if (u.name.equals(name)) return new Result<User>(u); } return new Result<User>(null); } // サンプル用mainメソッド public static void main(String[] args) { Users us = new Users(new User("hoge", 10), new User("piyo", 20)); Result<User> r = us.findByName("foo"); String msg = r.proc(new Result.Case<User, String>() { public String notNullCase(User u) { return String.format("name: %s, age: %d%n", u.name, u.age); } public String nullCase() { return ""; } }); System.out.print(msg); } }
こんな感じでしょうか。
汎用性を持たせるために、処理の戻り値の型も指定できるようにしたため、微妙にロジックを変えました。
Maybe について
やっと Maybe まで来ました。
といっても、さっきの Result クラスが Maybe です。
そう、名前変えただけです。
// ResultからMaybeに変えた(警告を潰すのは読者への課題とします。めんd・・・) public final class Maybe<E> { private final E result; // privateにして直接アクセスできないようにした private Maybe(E result) { this.result = result; } // new Maybe<E>(hoge)の代わりに使うメソッド public static <E> Maybe<E> just(E notNullValue) { return new Maybe<E>(notNullValue); } // new Maybe<E>(null)の代わりに使うメソッド public static <E> Maybe<E> nothing() { return new Maybe<E>(null); } public interface Case<E, R> { // nullCaseからifNothingに変えた R ifNothing(); // notNullCaseからifJustに変えた R ifJust(E result); } // procからmatchに変えた public <R> R match(Case<E, R> c) { if (this.result == null) return c.ifNothing(); return c.ifJust(this.result); } }
これを、ポリモーフィズム使って実現するとこうなります。
package maybe; public final class MaybeModule { private MaybeModule() {} public static <E> Maybe<E> just(E notNullValue) { return new Just<E>(notNullValue); } public static final Maybe NOTHING = new Nothing(); public static <E> Maybe<E> nothing() { return NOTHING; } public interface Maybe<E> { <R> R match(Case<E, R> c); } public interface Case<E, R> { R ifNothing(); R ifJust(E value); } // 値がある時 public static final class Just<E> implements Maybe<E> { private final E value; public Just(E value) { this.value = value; } @Override public <R> R match(Case<E, R> c) { return c.ifJust(this.value); } } // 値がない時 public static final class Nothing<E> implements Maybe<E> { @Override public <R> R match(Case<E, R> c) { return c.ifNothing(); } } }
一つ一つの public な class / interface を別ファイルに切り出すのが面倒なので MaybeModule を static import して使う想定です。
これを最終版として、もう一度 User の例を見てみます。
import java.util.*; import static maybe.MaybeModule.*; class User { /* 略 */ } public class Users { private final List<User> users = new ArrayList<User>(); public Users(User...users) { this.users.addAll(Arrays.asList(users)); } public Maybe<User> findByName(String name) { for (User u : this.users) { if (u.name.equals(name)) return just(u); } return nothing(); } // サンプル用mainメソッド public static void main(String[] args) { Users us = new Users(new User("hoge", 10), new User("piyo", 20)); Maybe<User> r = us.findByName("foo"); String msg = r.match(new Case<User, String>() { public String ifJust(User u) { return String.format("name: %s, age: %d%n", u.name, u.age); } public String ifNothing() { return ""; } }); System.out.print(msg); } }
just やら Just のコンストラクタやらに null を渡すとダメという点で、ポリモーフィズムなし版より残念なことになっていますが、just に関しては notNullValue が null なら nothing 呼び出しに切り替えることで回避可能です。
また、Just のコンストラクタは公開しなければどうということはありませんので、あまり重要な問題ではありません。
重要なのは、「Maybe は汎用的な Null Object と考えることができる」という点です。
恐れることは何もありません。
Java 版 Maybe についての考察
さて、Java で Maybe を作ってみました。
こいつと、他の手法を比較してみましょう。
null | 例外 | Maybe | |
---|---|---|---|
ぬるぽの恐怖 | ある | ない | ない |
チェックの漏れ | ある | 握り潰しの可能性 | ない*3 |
チェックが必要かどうかの判断 | ドキュメンテーションコメント | 同左*4 | 型*5 |
コーディング量 | 少 | 大 | 最大 |
効率*6 | 最速? | まぁまぁ? | 悪そう |
こんな感じでしょうか。
なぜ null より安全に「値が無いこと」を表せるのか、それは ResultUser を導入したバージョンに集約されています*7。
- User を直接触らせない
- 引数で渡ってくる User は null になり得ない
- 値が「ない」場合を考慮することをコンパイラが強制してくれる
ちょっとコード量が多すぎるのは、Java にラムダ式がないからです。
しかし、Java 8 からはラムダ式が乗ります。
これを使った場合どのようなコードになるのか見てみます。
※以降、微妙に難しいかも。ここまでで当初の目的である「なんで安全なの?」までは終わったので、以降はオマケとして読んでもらえれば。
ラムダ式
まず、Maybe はこう書き直します。
// Java8だよ! package functions; public FunctionModule { public enum Unit { _ } public interface Func1<A, R> { R apply(A arg); } }
// Java8だよ! package maybe; import static functions.FunctionModule.*; public final class MaybeModule { private MaybeModule() {} public static <E> Maybe<E> just(E notNullValue) { return new Just<E>(notNullValue); } public static final Maybe NOTHING = new Nothing(); public static <E> Maybe<E> nothing() { return NOTHING; } public interface Maybe<E> { // Caseインターフェイスは削除 // かわりに、Func1を受け取るように変更(Func0作って、それを使ってもよい) <R> R match(Func1<E, R> ifJust, Func1<Unit, R> ifNothing); } public static final class Just<E> implements Maybe<E> { private final E value; public Just(E value) { this.value = value; } // CaseインターフェイスのifJustメソッドではなく、Func1インターフェイスのapplyメソッドを呼び出すように変更 @Override public <R> R match(Func1<E, R> ifJust, Func1<Unit, R> _) { return ifJust.apply(this.value); } } public static final class Nothing<E> implements Maybe<E> { // Justと同じように、Func1インターフェイスのapplyを呼び出すように変更 @Override public <R> R match(Func1<E, R> _, Func1<Unit, R> ifNothing) { return ifNothing.apply(Unit._); } } }
使う側はこうなります。
// Java8だよ! import java.util.*; import static maybe.MaybeModule.*; import static functions.FunctionModule.*; // 必要か分からないけど一応 class User { /* 略 */ } public class Users { private final List<User> users = new ArrayList<User>(); public Users(User...users) { this.users.addAll(Arrays.asList(users)); } public Maybe<User> findByName(String name) { for (User u : this.users) { if (u.name.equals(name)) return just(u); } return nothing(); } // サンプル用mainメソッド public static void main(String[] args) { Users us = new Users(new User("hoge", 10), new User("piyo", 20)); Maybe<User> r = us.findByName("foo"); String msg = r.match( u -> String.format("name: %s, age: %d%n", u.name, u.age), _ -> ""); System.out.print(msg); } }
かなりスッキリしました。
これ、わざわざ変数で受ける必要ないので、
String msg = us.findByName("foo").match( u -> String.format("name: %s, age: %d%n", u.name, u.age) _ -> ""); System.out.print(msg);
でいいです。
ラムダ式版との比較
null チェックと、findByName が例外投げる場合と比べてみましょう。
// nullチェックの場合 User u = us.findByName("foo"); if (u != null) { System.out.printf("name: %s, age: %d%n", u.name, u.age); } // findByNameが例外を投げる場合 try { User u = us.findByName("foo"); System.out.printf("name: %s, age: %d%n", u.name, u.age); } catch (Exception e) { /* do nothing */ } // Maybe(Java8) String msg = us.findByName("foo").match( u -> String.format("name: %s, age: %d%n", u.name, u.age) _ -> ""); System.out.print(msg); // もしくはMaybeにmatchAndActというメソッドを追加して・・・(Java8) us.findByName("foo").matchAndAct( u -> { System.out.printf("name: %s, age: %d%n", u.name, u.age); }, _ -> {});
あれ、これアリじゃないですかね。
toString を実装している場合は、一番安全にもかかわらず、一番短く書けますね。
// nullチェックの場合 User u = us.findByName("foo"); if (u != null) { System.out.println(u); } // findByNameが例外を投げる場合 try { User u = us.findByName("foo"); System.out.println(u); } catch (Exception e) { /* do nothing */ } // Maybe(Java8) us.findByName("foo").matchAndAct(System.out::println, _ -> {});
Maybe の欠点
まだまだ考慮が足りてない部分とかあるとは思いますが、Java8 だと Maybe を使うのは割とアリなんじゃないでしょうか。
今度は、Java8 で書いた Maybe の欠点を見て行きます。
例えば、キーを 2 つ取って、db という Map からそれらに対応する値を取り出し、加算する関数を考えます*8。
// Java8だよ! import java.util.*; import static maybe.MaybeModule.*; import static functions.FunctionModule.*; public class Sample { private static Map<String, Integer> db = new HashMap<String, Integer>() {{ put("x", 1); put("y", 2); }}; private static Maybe<Integer> tryFind(String key) { if (db.containsKey(key)) return just(db.get(key)); return nothing(); } public static Maybe<Integer> plus(String key1, String key2) { // Justの場合は処理を続けるが、Nothingが現れたら以降の処理は実行せず、Nothingを返す return tryFind(key1).match( v1 -> tryFind(key2).match( v2 -> just(v1 + v2), _ -> nothing()), _ -> nothing()); } // サンプル用mainメソッド public static void main(String[] args) { Maybe<Integer> res1 = plus("x", "y"); // Just(3) Maybe<Integer> res2 = plus("x", "z"); // Nothing Maybe<Integer> res3 = plus("a", "y"); // Nothing Maybe<Integer> res4 = plus("a", "b"); // Nothing } }
ちょっとどうインデントすべきか分かりません。
また、加算対象となるキーが増えた場合を考えると・・・やめましょう。
ちなみに、例外を投げる場合は何も考えずに実装できます。
だって失敗したら例外が投げられるんだから、メソッドの最後に到達したらそれは key1 も key2 も対応する値があったということですからね。
null を返す場合は、チェックコードが散乱してしまって嬉しくないです。
バインド!
match のネストを解決するために、Maybe にメソッドを足しましょう。
// Java8だよ! package maybe; import static functions.FunctionModule.*; public final class MaybeModule { // 略 public interface Maybe<E> { <R> R match(Func1<E, R> ifJust, Func1<Unit, R> ifNothing); // bindメソッドを追加 // 「以降の処理」がrestとして渡される <R> Maybe<R> bind(Func1<E, Maybe<R>> rest) default { return match( v -> rest.apply(v), _ -> nothing()); } } // 略 }
このメソッドを使うと、先ほどのコードはこうなります。
// Java8だよ! import java.util.*; import static maybe.MaybeModule.*; import static functions.FunctionModule.*; public class Sample { // 略 public static Maybe<Integer> plus(String key1, String key2) { return tryFind(key1).bind( v1 -> tryFind(key2).bind( v2 -> just(v1 + v2))); } // 略 }
tryFind(key1) が Just ならそれに包まれた値が v1 に入り、さらに tryFind(key2) が Just ならそれに包まれた値が v2 に入り、just(v1 + v2) が実行されます。
途中で一つでも Nothing が返されれば、それ以降の処理は実行されず、全体として Nothing が返されます。
以降の式で使える変数 (v1 や v2) のインデントを合わせることができるため、先ほどよりは読みやすいかと思います。
ただし、通常は変数は
変数 = 値
// 変数を使った処理の続き
のように右から左に流れるイメージですが、bind はすべて左から右に流れます。
値.bind(変数 -> /* 変数を使った処理の続き */);
これを例えば Scala では
val db = Map("x" -> 1, "y" -> 2) def plus(key1: String, key2: String) = for { v1 <- db.get(key1) // getはtryFind相当 v2 <- db.get(key2) } yeild v1 + v2
と、通常の変数と同じ感覚で読める構文を提供しています。
Java も、Maybe に Iterable を実装して
public static Maybe<Integer> plus(String key1, String key2) { for (Integer v1 : tryFind(key1)) for (Integer v2 : tryFind(key2)) return just(v1 + v2); }
とできなくはないんですが、ううむ、ここまでやっていいものか・・・*9
それにこのコード、IDE はおそらく 2 つ目の for をインデントします。
もういっこ
先ほどの例では、Just が返される限り続けました (&& みたいな感じ)。
こんどは、最初の Just を返すものを見てみましょう (|| みたいな感じ)。
例えば、key1 に対応する値があればその値を Just に包んで、key2 に対応する値があればその値を Just に包んで、key3 に対応する値があればその値を Just に包んで、どちらもなければ Nothing を返す関数を考えてみます。
// Java8だよ! import java.util.*; import static maybe.MaybeModule.*; import static functions.FunctionModule.*; public class Sample { private static Map<String, Integer> db = new HashMap<String, Integer>() {{ put("x", 1); put("y", 2); put("z", 3); }}; private static Maybe<Integer> tryFind(String key) { if (db.containsKey(key)) return just(db.get(key)); return nothing(); } public static Maybe<Integer> search3key(String key1, String key2, String key3) { // Nothingの場合は処理を続けるが、Justが現れたら以降の処理は実行せず、Justで包んで返す return tryFind(key1).match( v1 -> just(v1), _ -> tryFind(key2).match( v2 -> just(v2), _ -> tryFind(key3).match( v3 -> just(v3), _ -> nothing()))); } // サンプル用mainメソッド public static void main(String[] args) { Maybe<Integer> res1 = search3key("x", "y", "z"); // Just(1) Maybe<Integer> res2 = search3key("x", "b", "z"); // Just(1) Maybe<Integer> res3 = search3key("a", "y", "z"); // Just(2) Maybe<Integer> res4 = search3key("a", "b", "c"); // Nothing } }
やはりつらいですね。
これ、今度は例外投げる場合がちょっとつらいです。
public static Integer search3key(String key1, String key2, String key3) { // 例外が投げられた場合は処理を続けるが、そうでなければ以降の処理は実行せず、そのまま返す try { return tryFind(key1); } catch (Exception e) { try { return tryFind(key2); } catch (Exception e2) { return tryFind(key3); } } }
逆に、null チェックの場合はガード節になるので非常に読みやすいコードとなります(でも安全ではない点には注意)。
えむぷらす!*10
これを解決するために、Maybe にまたメソッドを足しましょう。
// Java8だよ! package maybe; import static functions.FunctionModule.*; public final class MaybeModule { // 略 public interface Maybe<E> { <R> R match(Func1<E, R> ifJust, Func1<Unit, R> ifNothing); <R> Maybe<R> bind(Func1<E, Maybe<R>> rest) default { return match( v -> rest.apply(v), _ -> nothing()); } // orメソッドを追加 // 「以降の処理」がrestとして渡される Maybe<E> or(Func1<Unit, Maybe<E>> rest) default { return match( v -> just(v), _ -> rest(Unit._)); } } // 略 }
このメソッドを使うと、先ほどのコードはこうなります。
// Java8だよ! import java.util.*; import static maybe.MaybeModule.*; import static functions.FunctionModule.*; public class Sample { // 略 public static Maybe<Integer> search3key(String key1, String key2, String key3) { return tryFind(key1).or(_ -> tryFind(key2).or(_ -> tryFind(key3))); /* または、 return tryFind(key1).or(_ -> tryFind(key2)).or(_ -> tryFind(key3)); でも可 */ } // 略 }
すごいシンプルになりました!
情報が足りない・・・
例外の素晴らしい点の一つは、情報を豊富に保持することができる点にあると思います。
それに対して、Maybe はどうでしょう。失敗時に何が原因で失敗したのか全然分かりませんよね。
ということで、失敗時の情報を持つようにしましょう!
*1:えぇー、その getter、ドキュメンテーションコメントいらないように分かりやすい名前付けたのに!
*2:ただしリフレクションは考慮しないものとする
*3:握りつぶそうにも型を合わせる必要がある
*4:検査例外という手もあるにはある
*5:Maybe かどうかで判断できるうえ、コンパイラにも守ってもらえる
*6:測ったことないのでわからんけど(ぇ
*7:それ以降はより汎用的に使えるようにするための変更なので
*8:この例は http://d.hatena.ne.jp/mzp/20110205/monad からパクリました。このエントリも素晴らしいので後で読んでみてください。
*9:更に、for が文なので値が持てないのも微妙に痛い
*10:気にしないでください
アンダーバーって便利ですよね
public enum Unit { _ }
とか、
// 2要素タプル public final class _2<T1, T2> { ... } // 3要素タプル public final class _3<T1. T2, T3> { ... } ...
とか。
F のメソッド名も _ だし、Map リテラル用のメソッドも _ で。
Java で Map リテラル (もどき)
上のは冗談ですが、こっちは割と本気です。
コレクションのリテラルについては
コレクション (List, Set, Map 等) の生成の簡略化 - ぐるぐる~
とかで言及しているんですけど*1、Map については「これだ!」というものがありませんでした。
ですが、最近自分の中では答えが出ました。
public abstract class MapL<K, V> extends LinkedHashMap<K, V> { protected void _(K key, V value) { put(key, value); } }
こんなクラスを用意して、こう使います。
Map<Integer, String> m = new MapL<Integer, String>() {{ _(1, "hoge"); _(5; "piyo"); }};
ほんの少しだけ面倒ですけど、分かりやすい表記に思えなくもないです。
今気付いたんですが、F# の
let m = Map.ofList [(1, "hoge"); (5, "piyo")]
と微妙に似ていますね。だからどうだという話ではないですが。
で、これだと LinkedHashMap、HashMap、Map あたりにしか使えないので、実際には
public abstract class MapL<K, V> extends LinkedHashMap<K, V> { protected void _(K key, V value) { put(key, value); } public HashMap<K, V> toHashMap() { return new HashMap<K, V>(this); } public LinkedHashMap<K, V> toLinkedHashMap() { return new LinkedHashMap<K, V>(this); } public TreeMap<K, V> toTreeMap() { return new TreeMap<K, V>(this); } public TreeMap<K, V> toTreeMap(Comparator<? super K> comparator) { TreeMap<K, V> m = new TreeMap<K, V>(comparator); m.putAll(this); return m; } public ConcurrentHashMap<K, V> toConcurrentHashMap() { return new ConcurrentHashMap<K, V>(this); } public ConcurrentSkipListMap<K, V> toConcurrentSkipListMap() { return new ConcurrentSkipListMap<K, V>(this); } public ConcurrentSkipListMap<K, V> toConcurrentSkipListMap(Comparator<? super K> comparator) { ConcurrentSkipListMap<K, V> m = new ConcurrentSkipListMap<K, V>(comparator); m.putAll(this); return m; } }
こんな感じでどうでしょうか。
toString や equals や hashCode をオーバーライドするのもアリかもしれませんね!
TreeMap<Integer, Integer> m = new MapL<Integer, Integer>() {{ _(1, 2); _(3, 4) }}.toTreeMap();
Java で map や filter
現在の Java にはラムダ式が無いので、map や filter をやろうとすると割とひどいことになります。
例えば、
public interface F<Arg, Ret> { Ret _(Arg a); }
というインターフェイスを用意して、
public class Op { public static <T, U> List<U> map(List<T> xs, F<T, U> f) { List<U> result = new ArrayList<>(); for (T x : xs) { result.add(f._(x)); } return result; } public static <T> List<T> filter(List<T> xs, F<T, Boolean> cond) { List<T> result = new ArrayList<>(); for (T x : xs) { if (cond._(x)) result.add(x); } return result; } }
のように実装すると思います*1。
しかし、このようにインターフェイスを使ってしまうと、これを呼び出して使う側にこのようなコードを強制してしまいます。
static List<Integer> allPlus10(List<Integer> xs) { return Op.map(xs, new F<Integer, Integer>() { public Integer _(Integer x) { return x + 10; } }); }
これは不要な部分ばかりです。
- new F
() { ... } - public Integer _(Integer x) { ...}
- return
これはこれで便利なのですが、毎回毎回インデントが深くなりすぎる上に、要らないコードが多すぎて微妙に読みにくいです。
そこで、もういっそ汎用的な仕組みはあきらめたらいいんじゃないだろうか、ということで、こんなコードでどうでしょうか?
public class Map<T> extends ArrayList<T> { // このクラス名そのままはさすがにないか・・・ protected void _(T t) { add(t); } } public class Filter<T> extends ArrayList<T> { protected void _(T t) { add(t); } } static List<Integer> allPlus10(final List<Integer> xs) { // これをmapの構文と「思い込む」 return new Map<Integer>() {{ for (int x : xs) _(x + 10); }}; } static List<Integer> onlyEven(final List<Integer> xs) { // これをfilterの構文と「思い込む」 return new Filter<Integer>() {{ for (int x : xs) if (x % 2 == 0) _(x); }}; }
これだとノイズは
- for (int x : xs)
- _(...);
くらいなので、個人的には許容範囲内かなぁ、とか思わなくもないです。
filter は 要素の型と戻り値の型が同じなのに 2 回型を書くことになりますが、それでもインターフェイスでやるよりは少ないです。
map は要素の型と戻り値の型を一回ずつ書けばいいだけなので、インターフェイス版よりもお得感がより強い感じです。
ええっと、もちろん冗談です。
・・・冗談ですよ?
良い子のみんなは真似しないでネ!
*1:自分で cons セルなリストを作って、そのクラスに map メソッドを持たせる、とか、super や extends を頑張るというのもありです。
Java で順列リテラル?
このエントリは Java Advent Calendar -ja 2010 : ATND の 2 日目のものです。ネタです。
みなさん Java では true や false や null がキーワードではなく、リテラルだというのはご存じだと思います。
では、「え、そんなリテラルあるの?」というリテラルがあることはご存知でしょうか?
それは、「順列リテラル」です!
System.out.println(0x05P2); // 5! / (5-2)! = 20 System.out.println(0x01P0); // 1! / (1-1)! = 1
左側が 16 進数じゃなければならないのと、結果が double になってしまうのに目をつぶれば、超便利です!
・・・ごめんなさい。以下ネタバレ