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:気にしないでください
JSX の型を整理してみた
JSX の型はかなり複雑なことになっている気がしたので、整理してみました。
プリミティブ型、オブジェクト型、可変型、未定義許可型
JSX における型は、この 4 種類に分類されるらしいです。
プリミティブ型
プリミティブ型は現在、
- boolean
- int
- number
- string
の 4 種類があります*1。
これらの型を持つ変数には null を入れることができません。
var x: int = null; // compile error!
また、これらの型の値は変更不可能 (イミュータブル) となります。
3 がいつの間にか 4 に変わっていたりしてほしくないですよね?
"hoge" という文字列の o という文字がいつの間にか a に変わっていて "hage" とか悲しいですよね?
これらの型の値では、そのようなことは起こりません。
オブジェクト型
オブジェクト型は例えば、
- string 配列
- 関数
- Date
- Object
などが該当します。
class などによりユーザが定義する型は全てオブジェクト型となります。
プリミティブ型とは違い、これらの型を持つ変数には null を入れることができます。
var xs: int[] = null; // OK
また、プリミティブ型とは違い、オブジェクト型は全てがイミュータブルというわけではありません。
class ImmutableSample { var _x: int; function constructor(x: int) { this._x = x; } function x(): int { return this._x; } } class MutableSample { var _x: int; function constructor(x: int) { this._x = x; } function x(): int { return this._x; } function setX(x: int): void { this._x = x; } }
この例では、ImmutableSample のオブジェクトに対しては (_で始まるメンバにはアクセスしない、という約束事を守ると) 生成後にその状態を変更することはできません。
それに対して、MutableSample のオブジェクトに対しては、setX を呼び出すことで生成後にオブジェクトの状態を変更することができます。
可変型
可変型はコンパイル時に定まらない型のために用意されています。
この型の変数には null を入れることが可能です。
チュートリアルには、「この型の値に対してできるのは同じかどうかの判定だけで、何かやりたければキャストが必要になる」というようなことが書いてあります。
が、現在はプロパティアクセスなら許しているようです。
プロパティアクセスも禁止するようになったようです。不便になったと感じるかもしれませんが、自分としてはプロパティアクセスも不可の方が正しいと思います。
JavaScript とちがい、typeof 演算子は可変型のみに許されています*2。
また、可変型を表すキーワードには variant を使います。個人的には any 型とか別の言葉を使ってほしかったところです。
可変型は null を扱えますので、こんなことも可能です。
var v: variant = null; var i: int = v as int; log i; // => 0
boolean の場合 false に、string の場合は null という文字列に変換されるようです。
string の場合は空文字列とかになってほしいような気もします。
未定義許可型
未定義許可型は、未定義 (undefined) という値を取りうる唯一の型です*3。
例えば、0 での割り算を undefined にしたい場合、
static function div(a: int, b: int): MayBeUndefined.<int> { if (b == 0) return undefined; return a / b; }
このように、MayBeUndefined.
ただし、この T に MayBeUndefined 自身を指定するとコンパイルエラーになるようです。
また、可変型を指定してもコンパイルエラーとなります。
未定義許可型は、T の扱える範囲に加えて undefined が扱える型ですので、T がプリミティブ型であれば null は扱えませんし、オブジェクト型であれば null も扱える、という非常に特殊な型となっています。
var x: MayBeUndefined.<int> = null; // コンパイルエラー var y: MayBeUndefined.<Date> = null; // OK
その他特殊な型
JSX の型はプリミティブ型、オブジェクト型、可変型、未定義許可型の 4 種類と書きましたが、これらに (おそらく) 属さない特殊な型や、JSX 処理系によって特別扱いされている型もあります。
配列型
配列はオブジェクト型の属しますが、扱いはかなり特殊です。
配列の型は Array.
また、Array.
このあたり、Java などから入る人には罠だと思います。
これはサイズを指定した配列を作った時に、JavaScript では配列の各要素が undefined で埋められるのが主な原因と思われます。
Array.
それは、T に可変型が指定できることです*4。
未定義許可型の T には可変型が指定できない、と書きましたが、実はできる方の方が特殊なのです。
そのため、JSX では「なんでも入るコレクション」を作ることはできません。
追記:これは自分の勘違いでした。コメント欄参照。
他に不思議なこととして、
var xs = [1, 2, undefined]: int[]; var ys = new int[3]; ys[0] = 1; ys[1] = 2; log xs; // => [ 1, 2, undefined ] log ys; // => [ 1, 2, ]
となります。ううん・・・?
void 型
void 型は値を返さない関数の戻り値に指定します。
それ以外の場所では指定できません。
こんな感じで
JavaScript に引っ張られたりバグなのか仕様なのかよく分からん動きをしたりするので、割と複雑です。
ルールさえわかってしまえばどうということはないのですが、そういう文章が JSX はあまり整備されていませんので、勝手にまとめてみました。
参考資料
JSX の進化速度が半端ない
気に入らない所を直して pull request 投げたら、取り入れられたので、8 日前に書いたエントリが過去のものとなっちゃいました。
関数型
以前の JSX では、関数型は
function(: int): string
のように書く必要がありました。
これはこれでそのまま使えるのですが、新たに
(int) -> string
という形式にも対応しました。
ちなみに、複数引数はカンマ区切りで
(int, boolean) -> string
のようになります。
カリー化された関数は、
function(: int): function(: number): string
の代わりに
(int) -> (number) -> string
と書けます。
引数を囲むカッコは、(今のところ) 省略不可能です。
これには 2 つの理由があります。
- この機能を追加したとき、JSX のパーサの能力が LL(1) だと思っていた
- 引数を追加したときにわざわざカッコまで補うのが面倒
一つ目の理由はもうすでに消えており(_preserveState/_restoreState がある)、残るは二つ目の理由だけですが、これは開発者に委ねようと思います。
この部分に関しては現状で割と満足しています。
無名関数
先のエントリには書いていないのですが、従来の JSX では無名関数を書くために function キーワードが必要となっており、ちょっと面倒でした。
function f(x: int): (int) -> int { return function(y: int): int { return x + y; }; }
最後のセミコロンを忘れがちで JSX のコードを書いていると発狂しそうでしたが、こう書けるようになりました。
function f(x: int): (int) -> int { return (y: int): int -> x + y; }
function キーワードがなくなり、また無名関数の中身が return 文のみの場合、省略できます。
ちなみに、もちろん
function f(x: int): (int) -> int { return (y: int): int -> { return x + y; }; }
と書くこともできます。
ここまでが、取り入れられた部分です。
この記法は関数型の記法に合わせてみたのですが、関数型を返す関数とはこのままでは相性が悪いです。
function hoge(f: (int) -> (int) -> int): int { return f(10)(20); } log this.hoge((x: int): (int) -> int -> (y: int): int -> x + y);
ですが、これも型推論が搭載されたことにより、ある程度緩和されています。
型推論
型推論が少し強化されました。
例えば先ほどのプログラムは、
function hoge(f: (int) -> (int) -> int): int { return f(10)(20); } log this.hoge((x) -> (y: int): int -> x + y);
と、一番外側の型を省略できるようになりました。
このエントリを書いてから 1 時間と経たないうちに、外側だけでなく内側の型も推論されるようになりました。
そのため、
function hoge(f: (int) -> (int) -> (int) -> int): int { return f(10)(20)(30); } log this.hoge((x) -> (y) -> (z) -> x + y);
と記述できます。
以下古い情報です。
ただし、更に増えると
function hoge(f: (int) -> (int) -> (int) -> int): int { return f(10)(20)(30); } log this.hoge((x) -> (y: int): (int) -> int -> (z: int): int -> x + y);
となり、根本的な解決には至っていません。
これを解決するためには、カリー関数用に記法を拡張し、
// 未実装の機能 log this.hoge((x: int)(y: int)(z: int): int -> x + y + z);
のように書けるようにすると、許容できる範囲内に収まるような気がします*1。
まぁ、JSX はカリー関数を多用するような層をターゲットにしていないと思われるため、割とアリな選択のように思えます。
この型推論は、引数の位置でしか推論されないため、
function hoge(x: int): (int) -> int { return (y) -> x + y; }
のような推論はできません。
これは、型を明示して
function hoge(x: int): (int) -> int { return (y: int): int -> x + y; }
とする必要があります。
戻り値の位置でも推論されるようになり、
function hoge(x: int): (int) -> int { return (y) -> x + y; }
と書けるようになりました。
ループ地獄
型推論が強化されたことから、map など使いやすくなったかな?と思ったのですが、これはまだのようです。
template がユーザからも使えるようになったので、使ってみようと思いましたが、これもうまくいきませんでした。
class _Main { static function main(args: string[]): void { var hoge = new Hoge.<string, string>(); var xs = hoge.map(["hoge", "piyo"], (s) -> s + "."); log xs; } } class Hoge<T, U> { function map(xs: T[], f: (T) -> U): U[] { var len = xs.length; var ret = new U[len]; for (var i = 0; i < len; i++) ret[i] = f(xs[i]); return ret; } }
これはうまくいくのですが、string 以外*2でインスタンス化しようとしても失敗してしまいました。
多分バグなので、そのうち修正されると思います。
修正されたようです。以下のコードが動きます。
class _Main { static function main(args: string[]): void { var hoge = new Hoge.<number, string>(); var xs = hoge.map([10, 20], (n) -> n.toString() + "!"); log xs; // => [ '10!', '20!' ] } } class Hoge<T, U> { function map(xs: T[], f: (T) -> U): U[] { var len = xs.length; var ret = new U[len]; for (var i = 0; i < len; i++) ret[i] = f(xs[i]); return ret; } }
ただ、クラスじゃなくて関数レベルで型パラメータが取れないと使い勝手はよくないですね。
また、template として実装されているため、挙動が読みにくいのもちょっと難点かもしれません。
インスタンス化してみるまでエラーが分からないという点が、思いのほか使い勝手が悪かったです。
その他
色々触っているうちに、色々と見えてきましたがそれはまたの機会ということで。
なんかこのまま進むと JS (略) ハッカソンでやることがなくなりそうな勢い・・・
JSX のアレな所
注意!このエントリは既に古いので、JSX の進化速度が半端ない - ぐるぐる〜もあわせて読んでください。最新のコードを参照するのが手っ取り早いです。
JSX なる言語がリリースされました。
この言語が謳っているのが、
- 高速
- 安全
- 簡単(生産性が高い、とも)
という 3 点です。
高速と安全はまぁいいでしょう*1。
問題は、はたしてこの言語は簡単なのか?という点です。
簡単かどうかは人によるのでアレなのですが、まぁ一部の人にとっては簡単とは言えない (というか書く気がしない) 書き方を強制されるのです。
関数型
数値を受け取って文字列を返す関数を表す型は、JSX では以下のように書きます。
function(:number):string
これ単体で見ると分かりやすそうな気配はします。
では、これ読めますか?
function f(g: function(:number):number): function(:number):string { return function(x: number): string { return g(x).toString(); }; }
function という言葉に
- 関数の定義のキーワード
- 関数型のキーワード
- ラムダ式を作るためのキーワード
と、3重の意味を持たせてしまっているんですよね。
JavaScript は最初と最後の 2 つだったのですが、それにもう一個増えたことになります。
これを「読みやすい!簡単だ!」という人は、ごく少数なのではないでしょうか?
ここは、既存の言語と合わせて、
number -> string
としてほしかったです。
そうすれば、
function f(g: number -> number): number -> string { return function(x: number): string { return g(x).toString(); }; }
と、こちらの方がまだ読めるし書く気になりますよね。
ラムダ式の型推論
でも関数の定義部分で「戻り値は number 受け取って string 返す関数だ」と言っているわけですから、
function f(g: number -> number): number -> string { return function(x) { return g(x).toString(); }; }
と書けてもいいと思いませんか?俺は思います。
このくらいであれば、関数の定義部分での型指定が増える程度なので、許容範囲です。
欲を言うと、
// この関数の戻り値の型は関数本体から推論できる function f(g: number -> number) { // ここのラムダ式の戻り値の型は、toStringの戻り値がstringなので決定 // 引数のxは、gに渡していることからnumberで決定 // つまり、このラムダ式はnumber -> string // 以上のことから、この関数の戻り値の型は、number -> stringとわかる return function(x) { return g(x).toString(); }; }
のように、戻り値の型が明白な場合は省略できると個人的には嬉しいです。
ただ、これは「ルールが多くて複雑!」と思ってしまう人もいるようなので、強くは言いません。
ループ地獄
まぁ言いたいことの大半は上の 2 点と関連するのですが、生産性にかかわるであろう部分で分かりやすい実例をあげます。
それは、JSX ではループ地獄から逃れることができない、という点です。
例えば、JSX で number 配列のすべての要素に 10 足す関数を考えてみます。
static function add10(xs: number[]): number[] { var ret = new Array.<number>(xs.length); for (var i = 0; i < xs.length; i++) { ret[i] = xs[i] + 10; } return ret; }
これとは別に、string 配列のすべての要素の末尾に "." を追加する関数を考えます。
static function addDot(xs: string[]): string[] { var ret = new Array.<string>(xs.length); for (var i = 0; i < xs.length; i++) { ret[i] = xs[i] + "."; } return ret; }
この 2 つ、for の中でやってること以外は同じですよね。
こういう場合には map を使うのが常套手段です。
使ってみましょう。
static function add10(xs: number[]): number[] { return xs.map(function(x: MayBeUndefined.<number>): MayBeUndefined.<number> { return x + 10; }); } static function addDot(xs: string[]): string[] { return xs.map(function(x: MayBeUndefined.<string>): MayBeUndefined.<number> { return x + "."; }); }
え、本来書きたい部分じゃない所を書く量、そんなに減ってなくね・・・?
これはこう書きたいよなぁ・・・
// このくらい書けるならもはや関数化する意味もなし static function add10(xs: number[]): number[] { return xs.map(function(x) { x + 10; }); } static function addDot(xs: string[]): string[] { return xs.map(function(x) { x + "."; }); }
というわけで、JSX が簡単というのは言い過ぎだろうと思うんですがどんなもんでしょうか?
・・・と、ここまではライトな話で、以降はそうじゃない話です。
バリアントがすべてを台無しにしている
えっと、これ、静的型付きであることを売りにしているんですよね?
それなのになぜ、VB 的なバリアントが入っているんだ、と。
@nishio 型がぐだぐだじゃないものとのやりとりについてuntypedを導入する長期的な意義ってありますかね。「外界との I/O」については native class を書くのが安全だし、そうすべきだと思います。haXe でも JSX でも
2012-06-02 22:16:47 via Echofon to @nishio
と開発者の一人が untyped について述べてますが、それならこのバリアントはなんなの?と言いたいわけですよ。
untyped がダメなのであれば、バリアントはもっと駄目だと思うのです。
バリアントがない
あ、こっちのバリアントは VB 的なバリアントじゃなくて、直和型とかそんなあれのことです。
例えば haXe には enum という名前でこれが入っていて、enum に対する分岐に switch が使える等、これまで手続型のプログラミング言語を主に使ってきた人でも入りやすい文法になっています。
例えば haXe で
// JSXじゃないよ! enum Card { num(n: Int); jack; queen; king; joker; }
と書くと、
// JSXじゃないよ! function toInt(c: Card): Int { return switch (c) { case num(n): n; case jack: 11; case queen: 12; case king: 13; case joker: -1; } }
のように、switch 出来るものが簡単に作れます。
これは単純な Java などのような enum ではないことに注目してください。
num は他の列挙子と異なり、値を持っているのです。
これを、case の部分で num(n) とすることで、num が持っている値を取り出して使うことができます。
こういう、switch で使えるユーザ定義型を簡単に、しかも強力に*2サポートしているのは haXe の強みです。
ですが JSX にはバリアントはありません。
ではどうするか。はい。クラス階層を作りましょう、となるわけですね。
// JSXだよ! abstract class Card { abstract function toNum(): number; } class Num extends Card { var num: number; function constructor(n: number) { this.num = n; } override function toNum(): number { return this.num; } } class Jack extends Card { static var _instance = new Jack(); static function getInstance(): Jack { return Jack._instance; } override function toNum(): number { return 11; } } class Queen extends Card { static var _instance = new Queen(); static function getInstance(): Queen { return Queen._instance; } override function toNum(): number { return 12; } } class King extends Card { static var _instance = new King(); static function getInstance(): King { return King._instance; } override function toNum(): number { return 13; } } class Joker extends Card { static var _instance = new Joker(); static function getInstance(): Joker { return Joker._instance; } override function toNum(): number { return -1; } }
インスタンスメンバのアクセスに this が必須だったり、クラスメンバのアクセスにクラス名が必須なのもだるいと思いましたが、これはそんなレベルじゃないです。
まぁこの冗長さは自動生成で補うとしましょうか。
ですが、JSX には private とか readonly とかないので*3、せっかく静的型付き言語であるにもかかわらず、null の恐怖からは解放されません。
うーん、readonly あると最適化の余地も増えると思うんですけど、なんでこれないのかよく分かりません。
オーバーロード
オーバーロードできるよ! 静的型付きだもん!
JavaScriptみたいな最近噂の新言語、JSXのお話を聞いてきたよ!
とあるわけですが、静的型付き言語でオーバーロードをわざと許していない言語もあり、それには理由があるのです。
オーバーロードができるのは利点ではなく、単なるトレードオフです*4。
さて、JSX では + 演算子を関数として見たとき、どのような型を持っているのでしょうか?
少なくとも、以下の演算は出来るようです。
- int + int
- number + number
- int + number
- number + int
- string + string
つまり演算子がオーバーロードされているわけです。
ですが、これだと困ったことが出てきます。型推論に制限ができてしまうのです。
例えば、疑似言語Xでは + 演算子は int と int の足し算にしか使えないとしましょう。
その場合、以下の関数 hoge の型を推論することができるのです。
// 静的型付きの疑似言語X function hoge(a) { return a + 10; }
この関数は、int を 1 つ引数に取って、int を返す関数です(型を全く書いていないのに型が付く)。
演算子がオーバーロードされている JSX では、この関数の型は推論できません。
このように、オーバーロードを導入するのは単にトレードオフ*5でしかありません。
分かって導入したのであればいいのですが、「これ便利だから入れとこうぜ!」的なノリなのだとしたらその積み重ねは今後、機能同士の衝突となり、矛盾のない形での機能追加が困難になっていくでしょう。
Twitter を見てる限り、型推論を後で強化しよう、という思惑があるように感じたのですが、Scala を参考にすればある程度何とかなる・・・のかな?ちょっと分かりません。
色々書きましたが・・・
期待の言語ではあります。日本発ですし。
まだ生まれたばかりの言語ということもあるので、今後どうなっていくのか非常に楽しみです。
自分としては、何か一つでも突き抜けた言語になって行ってくれると嬉しいかな、と思っています。
すごい Haskell たのしく学ぼう!は本当にすごいのか?
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 19人 クリック: 552回
- この商品を含むブログ (36件) を見る
今話題の、すごい Haskell たのしく学ぼう!を読んだのですが、ちょっと思ったことがあるので書評と合わせて書いておきます。
思ったこと
関数型言語がこれほど話題になるのはとても嬉しいことです。
しかし、一方で懸念点もあります。
- ノリで「すごい」とだけ言う人たちがいる
- その人たちに乗せられて (自分には合わないのに) 買ってしまって、挫折してしまう人が出てきそう
この本は、いい本です。
翻訳の質も素晴らしく、読んでいて「読みにくいな」と思った部分はありません。
それに加え、訳注と Appendix も素晴らしい。
しかし、誰にでも勧めることのできる「すごい本」かというと、それは違うだろう、と思うのです。
なので、この本を読んで関数型の考え方や、Haskell が分からなくても大丈夫です、と言っておきたい。
正直、この本は簡単な本ではありません。
この 1 冊だけで理解しようとしてはいけません。
また、「Haskell を学びたいんじゃなくて、関数型のパラダイムを学びたいんだ。他の言語でも関数型の考え方を取り入れてプログラミングしたいんだ」という人にはお勧めしません。
「とにかく読めよ!」という態度の人に対しては、気にしないでおくか、具体的にどこがどうすごいのか聞いてみましょう。
答えられないのだとしたら、「とにかく読む」必要はありません。
この本を真に勧めたい人
まず第一に、Haskell を本当にやりたいと思っているというのが重要です。
「さらっと読んでエッセンスを取り入れよう」と思っている人はほぼ間違いなく撃沈されることでしょう。
加えて、この本はじっくりと、(出来れば実際にやりながら) 読むべきタイプの本です。
イラストはファンシーでゆるい雰囲気を漂わせていますが、内容はしっかりしています。
じっくり読める、という人には本当にお勧めできます。
ただ、もう一度言いますがこの本は簡単な本ではありません。
ですので、この本だけを読んで理解できなかったとしても落ち込まないでください。
何度も読み込むか、他の本も読んでみるか、勉強会などに参加して分からない所を聞くなどするといいでしょう。
この本じゃなくてもいいんじゃないか?という人
決してこの本を悪く言うのではなく、適材適所というものがあるでしょう、ということです。
関数型の考え方を取り入れたい!という人には、この本ではなくオブジェクト指向プログラマが次に読む本 ?Scalaで学ぶ関数脳入門あたりをお勧めします。
Scala で解説されているものの、がっつり Scala を解説しているわけではなく、関数型のパラダイムについての本となっています。
この本で Scala に興味を持ったのであれば、Scala実践プログラミング―オープンソース徹底活用もお勧めです。
そうではなく、関数型言語を何でもいいからやってみたい!という人には、プログラミングの基礎 (Computer Science Library)をお勧めします。
言語としては OCaml を使っていますが、これもやはりがっつり OCaml を解説しているわけではありません。
この本は教科書ですので、固いなぁと思う人もいるかもしれません。
そういう人には、実践 F# 関数型プログラミング入門をお勧めします。
1 章は色々と言いたいことがあるので、流し読みしてもらうとして、2 章から本格的にどうぞ。
エラッタの修正などが入ったバージョンがPDF版として販売されています。
言語としては F# を使っており、タイトル通り F# の本ですが、すごい Haskell よりもよりライトに読み下すことができるでしょう。
すごい H 本の読み方
他の関数型言語使いの方であれば、サクサク読めることでしょう。
そうではない、関数型言語初心者の方は、まずは 7 章までをしっかり理解しながら読んでください。
8 章でようやく Hello, World が出てきますので、そこをひとまずのゴールとして 7 章までをやりましょう。
そして 9 章でようやく、ある程度意味のあるプログラムを説明しています。
7 章までは分かったが、8 章と 9 章が難しい、という人は、上でも言ったように、他の本を読むなり誰かに聞くなりしましょう。
数をこなせば何か見えてくるかもしれないので、ふつうの Haskell などを読んでみるのはいいかもしれません。
IO について一つ、本書に無い説明を行うとすると、「IO アクションはコマンドパターンのようなものだ」です。
putStrLn などは、実際に画面に文字列を出力するのではなく、コマンドクラスのインスタンスを生成しているのだ、と考えてみてください。
main はそれを受け取ります。そして、main を呼び出すナニモノかがコンポジットパターン的に組み立てられたコマンドを実際に実行していく、みたいなメタファです。
感想
ラムダ式よりも先にセクションを説明するのは、なるほど、と感心しました。
ラムダ式が先に説明されていると、ラムダ式があればセクションを使わなくてもどうにでもなってしまうので、なかなかセクションを有効活用できないという事態に陥ってしまうという人がいます。
まぁ自分のことなんですが、セクションを自然に使えるようになるまでは割と苦労しました。
その点、この順番であればそういう心配はないので、これは素敵ですね。
それだけではなく、全体を通して説明の順番が自然なものになっています。
不満点はそれほどありませんが、色々と言葉が足りないかな、と思う所は多少ありました。
タプルに関してはやはりというかなんというか、ううむ。そんなにリストと隣り合わせて説明したいですかねぇ・・・まぎらわしいだけだと思うんですが。
後は、この本だけでバリバリ Haskell を使ってプログラムが書けるようになるかというと、そうではないと思うんですよね。
にも関わらず、「次どう進めばいいか」に対するポインタが何一つないのが気になりました。
感想としてはとりあえずはこんな感じでしょうか。
色々書きましたが、素晴らしい本であることは間違いないので、「Haskell を始めたい」と思ったのであれば是非読んでみてください。
きっと、良き指導書になってくれることでしょう。