不変オブジェクトで循環参照をつくる
お題:
新しい状態を作るにはcopyメソッドで新たなインスタンスを作ります。 そうすると結局お互いに保持している参照が古い状態を指していることになるので、参照を新しいものに付け替えなければなりません。 先の例ではvar属性だったため、再代入することで付けかけることができましたが、valになると新しいインスタンスを作らないと無理なのです。 ということで、これはループしてしまうので解決することはできません。そもそもvalだと相互参照の関連の取り扱いに無理があることがわかります。
http://j5ik2o.me/blog/2013/06/02/ddd-entity-reference/
内容から言えば全然本筋ではないのだけれど、不変オブジェクトで相互参照って不可能なのかなーと思っていろいろやってみたら出来たので書いてみる。
まあコードの中にIDによる参照が入るのがあんまり好きじゃない(そもそもフィールド自体が参照なので)というのもある。(ここの感想らへん参照)
シンプルな例
名前渡し(call-by-name)とlazy valを使うと、それぞれのフィールドが相互に参照し合うオブジェクトがつくれる。
クラス定義。コンストラクタの引数をcall-by-nameにして、その値をlazy valなフィールドの初期化に使っている。
class A(_b: => B) { lazy val b = _b } class B(_a: => A) { lazy val a = _a }
これに対して、以下のようにlazy valな変数を使ってnewしてやれば、相互に参照するオブジェクトがつくられる。
lazy val a: A = new A(b) lazy val b: B = new B(a) // aと、bの中のaは同じ? assert(a == a.b.a) printf("a\t= %s\n", a) printf("a.b.a\t= %s\n", a.b.a) // bと、aの中のbは同じ? assert(b == b.a.b) printf("b\t= %s\n", b) printf("b.a.b\t= %s\n", b.a.b)
出力も見てみる
a = A@13b84286 a.b.a = A@13b84286 b = B@26177587 b.a.b = B@26177587
ちゃんと相互に同じオブジェクトが参照されているようだ。
Employee/Departmentの例
真似して書いてみる
class Employee(val id: Long, val name: String, _department: => Option[Department] = None) { lazy val department = _department def copy(name: String = this.name, department: => Option[Department] = this.department) = new Employee(id, name, department) } class Department(val id: Long, val name: String, _employees: => Seq[Employee] = Seq.empty) { lazy val employees = _employees def copy(id: Long = this.id, name: String = this.name, employees: => Seq[Employee] = this.employees) = new Department(id, name, employees) }
idとnameはvalで、department、employeesは先ほどと同じように、引数をcall-by-nameで受けてlazy valなフィールドに使っている。copyメソッドも3つ目の引数をcall-by-nameにしている以外は同じ。
さて、新たなEmployeeに対してDepartmentをセットしながら、DepartmentのemployeesにそのEmployeeを追加するにはどうしたら良いか。
- Employeeのdepartmentをセット
- DepartmentのemployeesにEmployeeを追加
に加え、
- Departmentのemployeesの各要素のdepartmentを新しく作るDepartmentに差し替え
が必要になる。
関数で書くと次のようになる。
def addEmployeeToDepartment(employee: Employee, department: Department) = { // departmentの現在のemployeesの後ろにemployeeを追加し、 // 各自のdepartmentをnewDepartmentに差し替えたもの lazy val newEmployees: Seq[Employee] = (department.employees :+ employee) map (_.copy(department = newDepartment)) // departmentのemployeesをnewEmployeesに差し替えたもの lazy val newDepartment: Some[Department] = Some(department.copy(employees = newEmployees)) // 新しいDepartmentを戻す newDepartment.get }
:+で後ろに付けるのはコスト大きそうなので本当は逆順にしたりすると思う。
使いやすいようにDepartmentにメソッドの形で付ける。
class Department(val id: Long, val name: String, _employees: => Seq[Employee] = Seq.empty) { lazy val employees = _employees def copy(id: Long = this.id, name: String = this.name, employees: => Seq[Employee] = this.employees) = new Department(id, name, employees) def setEmployees(employees: Seq[Employee]) = { lazy val newEmployees: Seq[Employee] = employees map (_.copy(department = newDepartment)) lazy val newDepartment: Some[Department] = Some(copy(employees = newEmployees)) newDepartment.get } def addEmployee(emp: Employee) = setEmployees(employees :+ emp) }
使ってみる。
val department = new Department(1, "Dev") .addEmployee(new Employee(1, "KATO")) .addEmployee(new Employee(2, "BANDOU")) // departmentとdepartmentに属するemployeeのdepartmentが一致 assert(department.employees.forall(e => Some(department) == e.department)) // employeeはemployeeが属するdepartmentに属する assert(department.employees.forall(e => e.department.get.employees.contains(e)))
ちゃんと相互参照になっているようだ。
感想
今回は1対1、または、1対多の固定的なデータ構造の相互参照を作るという内容だったのでベタに書けたけど、これが内部に相互参照を含む任意のデータ構造に対してそのトポロジを維持しつつ一部を追加したり繋ぎ替えたりする、というような汎用的な形で実現できたらカッコイイかも(ZipperとかLensみたいな感じで。)
同じ名前のメソッドを持つクラスに対するimplicit defを2つ定義してimportした場合のコンパイルエラーがScala2.9と2.10で異なる件
「同じ名前のimplicit defを2つ定義してimportした場合の挙動がScala2.9と2.10で異なる件 - xuwei-k's blog」みて思い出したので確認してみた。
Scala 2.9.1 + scalaz 6.0.3で下のようなコードを書いてコンパイルすると "reassignment to val" エラーになる。
import scalaz._; import Scalaz._ object IsWriterMAOrMAB { def main(args: Array[String]) { writer("foo", 1) >>= (n => writer("bar", n)) } }
[error] /tmp/ScalaSample/src/main/scala/IsWriterMAOrMAB.scala:5: reassignment to val [error] writer("foo", 1) >>= (n => writer("bar", n)) [error] ^ [error] one error found [error] {file:/tmp/ScalaSample/}root/compile:compile: Compilation failed
implicit conversionsの適用時に曖昧でエラーが出た後、メソッド名が「=」で終わっているので代入演算子として再解釈しようとして"reassignment to val"というエラーになる模様。
scalazを使わず再現するコードを書くとこんな感じ。
object AmbiguousImplicitsSample { implicit def foo(n: Int) = new { def #=(m: Int) = m == n } implicit def bar(n: Int) = new { def #=(m: Int) = m == n } def main(args: Array[String]) { val i = 1 println(i #= 1) } }
メソッド内の処理の内容は特に関係ないです。
Scala 2.9.1でコンパイルすると……
[error] /tmp/ScalaSample/src/main/scala/AmbiguousImplicitsSample.scala:11: reassignment to val [error] println(i #= 1) [error] ^ [error] one error found [error] (compile:compile) Compilation failed
メソッド名を「eq」や「=#=」などに変えると、ちゃんと「曖昧なのでimplicit conversionsを適用しませんでした」エラーになる。
[error] /tmp/ScalaSample/src/main/scala/AmbiguousImplicitsSample.scala:11: type mismatch; [error] found : i.type (with underlying type Int) [error] required: ?{val =#=(x$1: ?>: Int(1) <: Any): ?} [error] Note that implicit conversions are not applicable because they are ambiguous: [error] both method foo in object AmbiguousImplicitsSample of type (n: Int)java.lang.Object{def =#=(m: Int): Boolean} [error] and method bar in object AmbiguousImplicitsSample of type (n: Int)java.lang.Object{def =#=(m: Int): Boolean} [error] are possible conversion functions from i.type to ?{val =#=(x$1: ?>: Int(1) <: Any): ?} [error] println(i =#= 1) [error] ^ [error] one error found [error] (compile:compile) Compilation failed
[error] /tmp/ScalaSample/src/main/scala/AmbiguousImplicitsSample.scala:11: type mismatch; [error] found : i.type (with underlying type Int) [error] required: ?{def #=(x$1: ? >: Int(1)): ?} [error] Note that implicit conversions are not applicable because they are ambiguous: [error] both method foo in object AmbiguousImplicitsSample of type (n: Int)AnyRef{def #=(m: Int): Boolean} [error] and method bar in object AmbiguousImplicitsSample of type (n: Int)AnyRef{def #=(m: Int): Boolean} [error] are possible conversion functions from i.type to ?{def #=(x$1: ? >: Int(1)): ?} [error] println(i #= 1) [error] ^ [error] /tmp/ScalaSample/src/main/scala/AmbiguousImplicitsSample.scala:11: value #= is not a member of Int [error] println(i #= 1) [error] ^ [error] two errors found [error] (compile:compile) Compilation failed
implicit conversionsの適用時に曖昧でエラーが出た後、「=」で終わるメソッド名でも、代入演算子として再解釈せずにエラーになりコンパイルが停止する模様。
これは多分改善されているのだと思う。まあどっちにしてもコンパイルエラーなので特に困ることはないと思うけど。
JavaでOutOfMemoryErrorを出す方法
お題:
結合テスト中のシステムで、OutOfMemoryErrorが発生しました。UT後ソースコードの変更はしていません。ヒープメモリは足りているようです。原因として何が考えられますか?(筆記解答)
8つの質問で、Java SI業界の現状を知る - レベルエンター山本大のブログ
OSのswapが足りないとOutOfMemoryError後に死んだりするみたいですね。
「Out of Memory Errors a list of all of them」の15ページ目致命的なOOMEの例が幾つも載っているけど、それぞれどういう時に出るかを面接で聞かれても答えられないかも……。
今回は折角なので致命的じゃない方の、普通にヒープメモリが足りない場合について実際に発生させてみたい。
java.lang.OutOfMemoryError: Java heap space
普通にヒープメモリが足りなくなるヤツ。
// クラッシュするコード List<byte[]> buffers = new ArrayList<byte[]>(); while (true) { buffers.add(new byte[1024 * 1024 * 1024]); }
// クラッシュするコード StringBuilder sb = new StringBuilder(); while (true) { sb.append("1234567890"); }
どんどんヒープを消費して行けばそのうちなくなります。参照が維持されているのでGCされない。
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
超でっかい配列を作ろうとして怒られるパターン。
// クラッシュするコード byte[] buffer = new byte[Integer.MAX_VALUE];
java.lang.OutOfMemoryError: unable to create new native thread
スレッド作り過ぎパターン。10秒の間に全力でスレッド作るので多分足りなくなる。Cヒープが枯渇してもOOMEになる。
// クラッシュするコード while (true) { new Thread() { @Override public void run() { try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); }
java.lang.OutOfMemoryError: GC overhead limit exceeded
ヒープサイズを大きく取りすぎたりして、GCに時間がかかり過ぎ&あんまり回収出来ない場合に出るそうですが、上手く出す方法見つからなかった。
いろいろ試してたらマシンが2〜3回死んだ。
java.lang.OutOfMemoryError: PermGen space (Javassist版)
「Javaでヒープ領域を余らせたままOutOfMemoryErrorを出す方法 - 西尾泰和のはてなダイアリー」でやられているように、クラスを沢山作ってクラスローダを開放しないと、JavaヒープのPermanent領域が枯渇して出る。
バイトコード書くの大変なので、Javassistとか使っちゃう。
package sample.oom; // w/ javassist-3.4.ga.jar import javassist.ClassPool; // クラッシュするコード public class OOMSample { public static void main(String[] args) { new Exception().printStackTrace(); try { ClassPool pool = ClassPool.getDefault(); for (int i = 0; i < Integer.MAX_VALUE; i++) { String className = String.format("MyClass%d", i); pool.makeClass(className).toClass(); } } catch (Exception e) { e.printStackTrace(); } } }
結果:
daphne:JavaSample terazzo$ java -cp bin:lib/javassist-3.4.ga.jar sample.oom.OOMSample java.lang.Exception at sample.oom.OOMSample.main(OOMSample.java:7) Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at javassist.ClassPool.toClass(ClassPool.java:904) at javassist.ClassPool.toClass(ClassPool.java:847) at javassist.ClassPool.toClass(ClassPool.java:805) at javassist.CtClass.toClass(CtClass.java:1037) at sample.oom.OOMSample.main(OOMSample.java:12)
「new Exception().printStackTrace();」は入れておかないとスタックトレース出力中に再度OOMEがおこるので入れてます。
起動オプションに「-XX:+TraceClassLoading」「-XX:+TraceClassUnloading」を付けると、クラスのロード/アンロードの様子がログで出力されて分かりやすい。
daphne:JavaSample terazzo$ java -XX:+TraceClassLoading -XX:+TraceClassUnloading -cp bin:lib/javassist-3.4.ga.jar sample.oom.OOMSample [Opened /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] ... [Loaded sun.reflect.NativeMethodAccessorImpl from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] [Loaded sun.reflect.DelegatingMethodAccessorImpl from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] [Loaded MyClass0 from __JVM_DefineClass__] [Loaded MyClass1 from __JVM_DefineClass__] [Loaded MyClass2 from __JVM_DefineClass__] [Loaded MyClass3 from __JVM_DefineClass__] [Loaded MyClass4 from __JVM_DefineClass__] [Loaded MyClass5 from __JVM_DefineClass__] ... [Loaded MyClass78596 from __JVM_DefineClass__] [Loaded MyClass78597 from __JVM_DefineClass__] [Loaded MyClass78598 from __JVM_DefineClass__] Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at javassist.ClassPool.toClass(ClassPool.java:904) at javassist.ClassPool.toClass(ClassPool.java:847) at javassist.ClassPool.toClass(ClassPool.java:805) at javassist.CtClass.toClass(CtClass.java:1037) at sample.oom.OOMSample.main(OOMSample.java:12)
java.lang.OutOfMemoryError: PermGen space (Rhino版)
Rhinoは内部でJavaScriptをJavaのクラスに変換しているので、やはり作りすぎて開放しないとPermGen spaceを食いつぶしてしまう。
package sample.oom; import java.util.ArrayList; import java.util.List; // w/ rhino1_7R2.zip import org.mozilla.javascript.Context; import org.mozilla.javascript.Script; // クラッシュするコード public class RhinoOOMSample { public static void main(String[] args) { Context context = Context.enter(); try { List<Script> scripts = new ArrayList<Script>(); while (true) { // JavaScriptコード「""」をコンパイルし、意図的にリークさせる scripts.add(context.compileString("", null, 0, null)); } } finally { Context.exit(); } } }
結果:
daphne:JavaSample terazzo$ java -cp bin:./lib/js.jar sample.oom.RhinoOOMSample Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass(ClassLoader.java:791) at org.mozilla.javascript.DefiningClassLoader.defineClass(DefiningClassLoader.java:62) at org.mozilla.javascript.optimizer.Codegen.defineClass(Codegen.java:146) at org.mozilla.javascript.optimizer.Codegen.createScriptObject(Codegen.java:101) at org.mozilla.javascript.Context.compileImpl(Context.java:2409) at org.mozilla.javascript.Context.compileString(Context.java:1359) at org.mozilla.javascript.Context.compileString(Context.java:1348) at sample.oom.RhinoOOMSample.main(RhinoOOMSample.java:16)
ログあり版
daphne:JavaSample terazzo$ java -XX:+TraceClassLoading -XX:+TraceClassUnloading -cp bin:./lib/js.jar sample.oom.RhinoOOMSample [Opened /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] ... [Loaded org.mozilla.javascript.DefiningClassLoader from file:/tmp/JavaSample/lib/js.jar] [Loaded org.mozilla.javascript.SecurityUtilities from file:/tmp/JavaSample/lib/js.jar] [Loaded org.mozilla.javascript.SecurityUtilities$2 from file:/tmp/JavaSample/lib/js.jar] [Loaded org.mozilla.javascript.gen.c1 from file:/tmp/JavaSample/lib/js.jar] [Loaded org.mozilla.javascript.gen.c2 from file:/tmp/JavaSample/lib/js.jar] [Loaded org.mozilla.javascript.gen.c3 from file:/tmp/JavaSample/lib/js.jar] ... java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:791) at org.mozilla.javascript.DefiningClassLoader.defineClass(DefiningClassLoader.java:62) at org.mozilla.javascript.optimizer.Codegen.defineClass(Codegen.java:146) at org.mozilla.javascript.optimizer.Codegen.createScriptObject(Codegen.java:101) at org.mozilla.javascript.Context.compileImpl(Context.java:2409) at org.mozilla.javascript.Context.compileString(Context.java:1359) at org.mozilla.javascript.Context.compileString(Context.java:1348) at sample.oom.RhinoOOMSample.main(RhinoOOMSample.java:16) [Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/jre/lib/rt.jar]
リフレクションが使われているので、使い方によってはスクリプト本体に加えてsun.reflect.Generated〜みたいなリフレクションの高速化の為のクラスも沢山作られるようだ。
Blocksで不動点関数(Re: リークしない再帰blocksの書き方)
お題:
Sometimes, you want a block to be able to call itself. That means it needs a reference to itself. And that means you have a wonderful opportunity to create a strong reference cycle that will endure till the end of time, or at least till your app exits.
Leak-Free Recursive Blocks - Jeremy W. Sherman
(via: 「リークしない再帰blocksの書き方 | Cocoaの日々情報局」)
Blocksを再帰的に呼び出す際に変数を経由すると、参照サイクルが出来てメモリリークの元になるらしい。経由する変数を弱参照(__weak __block)にすることでリークを避けるというのが一般的なテクニックのようだ。
変数を参照するから参照カウントが問題になるんなら、変数に代入しないで再帰呼び出しすれば良いと思ったのでやってみた。今回の解決策はあんまり実用的じゃないかもしれないけど、こんな方法もあるよということで。
あ、環境がいまだに Lion(10.7.5) + Xcode Version 4.3.3なのでバージョンとかによってはうまくいかないかも。
例題: 階乗計算
普通の関数で再帰を使って階乗計算するプログラムを書くと次のようになる。
long factorial(long n) { if (n == 0) return 1; return n * factorial(n - 1); }
10! = 10 * 9! = 10 * (9 * 8!) = ...という計算を、factorialの中からfactorialを再帰的に呼び出すことで実現している。
これをBlocksで書きたい場合……
long (^factorial)(long) = ^long(long n) { if (n == 0) return 1; return n * factorial(n - 1); // ここではまだfactorialは初期化されていない };
これを"factorial"という関数名を参照することなく書くにはどうしたら良いだろう。
不動点関数を使う
「メモ化(memoization), 再帰関数定義関数, 最小不動点」を参考に、不動点関数fixを真似して書いてみる。fix自体は普通の関数で書くよ。
// 引数がlong一つで戻り値もlongのBlocks typedef long (^simple_block)(long); // simple_blockを受け取りsimple_blockを返すBlocks typedef simple_block (^simple_block_maker)(simple_block); // 不動点関数: simple_block_makerを受け取りsimple_blockを返す simple_block fix(simple_block_maker maker) { return maker(^(long n) { return fix(maker)(n); }); }
分かりやすいようにtypedefしたけど、無くても書けなくもない。
で、使う側はこうなる。
simple_block factorial = fix(^simple_block(simple_block next) { return ^long(long n) { if (n == 0) return 1; return n * next(n - 1); }; }); NSLog(@"10! = %ld", factorial(10));
fixに対して、「Blocksを渡されるとBlocksを生成して返すBlocks」を渡してやる。fix内では、この「Blocks生成器」を引き回しながら、必要な分だけ順次Blocksを生成して呼び出すことで、再帰的な呼び出しを実現している。参照という意味ではサイクルじゃなくて線形になっているので参照サイクルがおこらない(たぶん。) これでメモリリークしないはず(たぶん。)
不動点関数もBlocksで書く
fixは普通の関数だから自分自身を参照しても問題ない、というのはなんかちょっとずるい(反則?)感じもするので、fixもBlocksで書いてみる。
前にJavaで書いたときに型を調べたので、それにあわせてtypedefするよ。(Javaのに合わせてちょっと名前変えた。)
// 引数がlong一つで戻り値もlongのBlocks typedef long (^func)(long); // funcを受け取りfuncを返すBlocks typedef func (^func_factory)(func); // func_makerを受け取りfuncを返すBlocks // typedef func (^func_maker)(func_maker); // こう書きたいけど…… typedef func (^func_maker)(id); // idでごまかす
typedefで自分自身を引数に取る関数は普通には定義出来ない*1ので、一旦idで置いて使う時にキャストするようにする。
これを使ってfixを書き直すと、次のようになる。
func (^fix)(func_factory) = ^func(func_factory factory) { return ^func(id maker) { return factory(^long(long n){ return ((func_maker) maker)(maker)(n); }); }(^func(id maker) { return factory(^long(long n){ return ((func_maker) maker)(maker)(n); }); }); };
いわゆるYコンビネータってヤツですナ。Yコンビネータについては「Y コンビネータって何? - IT戦記」とかを参照すると良いかも。
id型で受け取ったmakerは呼び出しの際にfunc_makerにキャストしてる。まあちょっと苦しいけど……。
呼び出す側は、型の名前を変えた以外は同じ。
func factorial = fix(^func(func next) { return ^long(long n) { if (n == 0) return 1; return n * next(n - 1); }; }); NSLog(@"%ld", factorial(10));
一回しか使わないなら、Blocksを変数に入れる必要も無い。
long ret = ^func(func_factory factory) { return ^func(id maker) { return factory(^long(long n){ return ((func_maker) maker)(maker)(n); }); }(^func(id maker) { return factory(^long(long n){ return ((func_maker) maker)(maker)(n); }); }); }(^(long (^next)(long)) { return ^long(long n) { if (n == 0) return 1; return n * next(n - 1); }; })(10); NSLog(@"%ld", ret);
他の型のBlocksにも使えるようにする
longを受け取ってlongを返すもの専用だと汎用性が低いので、long以外を受け取ったり、複数の引数を使ったり出来るようにしたい。
とりあえず、typedefしてる部分を展開してみる。
long (^(^fix)(long (^(^)(long (^)(long)))(long)))(long) = ^(long(^(^f)(long(^)(long)))(long)) { return ^(id g) { return f(^(long m){ return ((long(^(^)(id))(long)) g)(g)(m); }); }(^(id g) { return f(^(long m){ return ((long(^(^)(id))(long)) g)(g)(m); }); }); };
比較しやすいように参考サイトの表記に合わせて、func_factoryだったところを「f」に、func_makerだったところを「g」にしたよ。若干楽しそう(^(^)
コード内に、戻り値の型、引数の型、仮引数の宣言、呼び出し時の引数が含まれているので、その辺を取り替えられるようにしたい。思い切ってマクロにしてみる。えい!
#define FIX(RET_TYPE, PARAM_TYPES, FORMAL_PARAMS, CALL_PARAMS) \ (^(RET_TYPE(^(^f)(RET_TYPE(^)PARAM_TYPES))PARAM_TYPES) { \ return ^(id g) { \ return f(^FORMAL_PARAMS{ \ return ((RET_TYPE(^(^)(id))PARAM_TYPES) g)(g)CALL_PARAMS; \ }); \ }(^(id g) { \ return f(^FORMAL_PARAMS{ \ return ((RET_TYPE(^(^)(id))PARAM_TYPES) g)(g)CALL_PARAMS; \ }); \ }); \ }) // 階乗 long (^factorial)(long) = FIX(long, (long), (long m), (m)) (^(long (^next)(long)) { return ^long(long n) { if (n == 0) return 1; return n * next(n - 1); }; }); NSLog(@"10! = %ld", factorial(10)); // 3628800 // 互除法 int (^gcd)(int,int) = FIX(int, (int, int), (int m, int n), (m, n)) (^(int (^next)(int, int)) { return ^int(int m, int n) { if (n == 0) return m; return next(n, m % n); }; }); NSLog(@"gcd(246, 654) = %d", gcd(246, 654)); // 6
*1:structを経由するテクニックがあるっぽいけど、Blocksだとちょっと面倒くさい
Play2.0でパラメータを型安全に受け取る
アクションに渡すときはパスやクエリストリングはアクションの引数の形に変換する必要がありますが、その変換ルールを定めているのが PathBindable[T], QueryStringBindable[T] といった implicit object です。conf/routes では例えば Int などはではデフォルトで変換してくれます。これは QueryStringBindable[Int] を Play 側が定義してくれているからです。
Play 2.x の QueryStringBindable, PathBindable について - tototoshi の日記
これはいいですね。
とはいえ個人的にはパラメータのIDをIntやLongで受け取るだけでは不十分で、折角なんでもうちょっと変換してしまいたい。
例えば、computer-databaseのサンプルで、コンピュータの一覧から一つを選択して編集するところは次のようになっている。
ルーティング部分[ソース]:
# Edit existing computer GET /computers/:id controllers.Application.edit(id:Long)
HTMLテンプレートでリンクを表示する箇所[ソース]:
@computers.map { case (computer, company) => { <tr> <td><a href="@routes.Application.edit(computer.id.get)">@computer.name</a></td> <td> ...
/** * Display the 'edit form' of a existing Computer. * * @param id Id of the computer to edit */ def edit(id: Long) = Action { Computer.findById(id).map { computer => Ok(html.editForm(id, computerForm.fill(computer))) }.getOrElse(NotFound) }
このアクションはComputerのidしか取らないのに、パラメータをLongで受けてComputer.findByIdで自分で変換している。また、テンプレートの方ではcomputer.id.getでidをLongで取得して使用している。折角単一のルールからルーティング/リバースルーティングしているのに、二カ所ともLongを経由することで誤った型のデータを渡してしまう危険性がある。
id: Longの代わりにモデルのデータ型で受け取る
そこで、PathBindableを使ってComputer型で受け取れるようにするよ。*1
まず、参照先を真似してBuild.scalaにroutesImportを追加
val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings( routesImport += "controllers.Implicits._" )
Implicitsに、Computer用のPathBindableを定義
package controllers import models.Computer import play.api.mvc.PathBindable object Implicits { implicit object bindableComputer extends PathBindable[Computer] { def bind(key: String, value: String): Either[String, Computer] = { try { Computer.findById(value.toLong).toRight("Computer not found for parameter %s: %s".format(key, value)) } catch { case e: Exception => Left("Cannot parse parameter %s as Computer: %s".format(key, e.getMessage)) } } def unbind(key: String, value: Computer): String = value.id.toString() } }
すると、routes以下は次のように書ける
ルーティング(routes):
... # Edit existing computer GET /computers/:computer controllers.Application.edit(computer: models.Computer) ...
HTMLテンプレート(list.scala.html):
... @computers.map { case (computer, company) => { <tr> <td><a href="@routes.Application.edit(computer)">@computer.name</a></td> <td> ...
アクション(Application.scala):
def edit(computer: Computer) = Action {
Ok(html.editForm(computer.id.get, computerForm.fill(computer)))
}
それぞれがかなりすっきりした上に、ルーティング/リバースルーティングが同一の型に対しておこなわれることが保証されるようになった!
但し、存在しないIDを指定した場合に、以前はNot Foundを返していたところがBad requestになってしまうので少し仕様は変わってしまっている。BindがEither[String, A]じゃなくてEither[play.api.mvc.Result, A]とか返せると良いかも。
id: Longとモデルの型の中間的な値で受け取る
アクションは型安全にしたいけど、DBアクセスのタイミングは自分で管理したい、というなら、型パラメータでモデルの型を取れるように中間的な型を作ってラップしてやれば良い。
仮にResourceId[A]型とするよ。
package models trait ResourceId[A] { def encode(key: String): String def decode: Option[A] }
で、ResourceId型とエンコード文字列やモデルの型をマッピングするものを用意する
package models trait ResourceMapper[A] { def fromValue(value: A): ResourceId[A] def fromCode(key: String, code: String): ResourceId[A] } object ResourceMapper { def toId[A: ResourceMapper](value: A) = implicitly[ResourceMapper[A]].fromValue(value) }
Implicitには、モデルの型ごとにResourceMapperを用意してやれば良い
package controllers import models.Computer import play.api.mvc.PathBindable import models.ResourceMapper import models.ResourceId object Implicits { // ResourceId[A]から/に変換するPathBindable // implicitにResourceMapper[A]が必要 implicit def bindableResource[A: ResourceMapper] = new PathBindable[ResourceId[A]] { def bind(key: String, value: String): Either[String, ResourceId[A]] = Right(implicitly[ResourceMapper[A]].fromCode(key, value)) def unbind(key: String, id: ResourceId[A]): String = id.encode(key) } // implicitにResourceMapper[Computer]を用意 implicit object computerResourceMapper extends ResourceMapper[Computer] { def fromValue(value: Computer): ResourceId[Computer] = new ResourceId[Computer] { def encode(key: String): String = value.id.toString() def decode: Option[Computer] = Some(value) } def fromCode(key: String, code: String): ResourceId[Computer] = new ResourceId[Computer] { def encode(key: String): String = code lazy val value_ = { try { Computer.findById(code.toLong) } catch { case e: Exception => None } } def decode = value_ } } }
これを使うとroutes以下各ファイルは次のようになる
ルーティング(routes):
... # Edit existing computer GET /computers/:id controllers.Application.edit(id: models.ResourceId[models.Computer]) ...
HTMLテンプレート(list.scala.html):
@import models.ResourceMapper.toId @import controllers.Implicits._ ... @computers.map { case (computer, company) => { <tr> <td><a href="@routes.Application.edit(toId(computer))">@computer.name</a></td> <td> ...
アクション(Application.scala):
def edit(id: ResourceId[Computer]) = Action {
id.decode.map{ computer =>
Ok(html.editForm(computer.id.get, computerForm.fill(computer)))
}.getOrElse(NotFound)
}
まあ回りくどいけど、ResourceMapperをDBのメタデータから自動で定義出来たりするなら良いかも。
あと、データのコード表現を扱うという点で、フォームの中身をレンダリング/パースする際のMappingやFormatterととても近いと思うので、うまく統合出来ないかと思う。
感想
PathBindableやQueryStringBindableを使ってパラメータを型安全に扱えるのはとても良いと思う。
が、型安全が目的というより、データのコード表現を扱う部分をなるべく無駄なくというのが個人的にはやりたい。過去のエントリで言うとこの辺。
- Play2.0でセレクトボックスにEnumを貼れるようにする - terazzoの日記
- コード値をEnumを使って安全に扱う - terazzoの日記
- コード値の表示をtaglibでおこなう - terazzoの日記
- 折角なので今考えていること - terazzoの日記
例えばデータのリストをハイパーリンクの一覧としてレンダリングし、その一つをユーザがクリックすることでデータを選ぶという場合、大事なのはデータのリストからデータを一つ選ぶということであって、ハイパーリンクのURLの書式・コード表現は主な関心事ではないはずなんだよね。単にたまたまHTMLというメディアの上でデータを識別するのに文字列の形で表現されているにすぎない。
サーバから送信する際にデータを文字列としてエンコードし、選択した文字列が送り返されて来てサーバで受信されるというのは、丁度データのシリアライズ/デシリアライズ(マーシャリング /アンマーシャリング)と考えることが出来る。そう考えると、データのシリアライズの度に毎回シリアライズの形式を考えたりしないのと同じで、URLのパラメータやフォームのコード値やフォーマットを毎回考えなくても良いようにしたい。
あと、今回はサーバ上のデータとWebの関係だったわけだけど、同じことをRDBとサーバ上のプログラムの間でも言いたい。具体的には(自然キーではない)PKやFKをアプリケーションプログラム上から排除したい。DBのIDキーっていうのは、テーブル上でレコードを参照し特定するものなのだが、プログラム上でデータへの参照はフィールドなどでおこなわれているし、データの同一性は比較演算子などで実現されるのでIDをアプリケーションのコード上で直接扱うことは不要だと思う*2。クエリを組み立てる時にDSLを使って例えば「emp1.dep_id eq dep1.id」とかいうのは回りくどいので「emp1.dep eq dep1」みたいに書けるようにしたい。これはそのうちに。
と、ここ数年そういう問題意識でやってきたので「衝撃的なデータベース理論・関手的データモデル 入門 - 檜山正幸のキマイラ飼育記 (はてなBlog)」には大注目ですよ。理解出来ると良いのだけど。
*1:書き終わってから見たらAPIリファレンスにそのものずばりなサンプルが載ってるけど…… cf: http://www.playframework.com/documentation/api/2.1.0/scala/index.html#play.api.mvc.PathBindable
はてなブックマークの新デザイン
みんな怒ってるのは、使い勝手を変更したからでもオシャレだからでもなくて、端的に使いにくいからだと思うんだよね。
新着エントリのタイトルを1日1500件分ぐらい流し見している程度のユーザとして感想と希望*1を書いていくよ。
ちなみに我慢して使ってたら自分はもう結構慣れた。
一覧性について
トップは知らないけど、新着エントリの画面あたりの情報量って実はあんまり変わってないような気がする。旧デザインでも4〜5件ぐらいしか表示されてなかったので、頻繁にスクロールしないといけなかった。前回のデザイン変更の時はページあたりの件数が減って非常に不便になったけど、今回はあまり変わらないように思う。テンポ的にも、4件分ざっとみてスペースキー、の繰り返しなのであんまり変わってない。
現デザインはオートページャーがついてないので20件ごとにリンクをクリックする必要があり、そこはテンポが悪いと思う。オートページャーを復活させるか、次ページリンクにキーボードショートカットを付けたりすると、自分は嬉しいかも。
タイル内の要素の順番について
もうすでにあちこちで指摘されてるけど、画像ある場合と無い場合でタイトルの位置が変わるので、横方向に見ていくのに非常に見にくい。
あと、自分はサイト情報(ドメインだけじゃなくて、ブログの場合はユーザ名なども含まれている)とタグも結構見てるので、この辺とタイトルはなるべく近くに置いて欲しい。
ブックマーク数/カテゴリ&日付/タイトル/サイト情報/タグ/画像/サマリぐらいの順番になってると見ていきやすいと思う。
その他要素について
タイトルとサマリだけ見てブックマークすることがあるので、B!ボタンは復活させて欲しい。
タイトルが長過ぎる時に末尾「...」で省略されるけど、以前はtitle属性にフルのタイトルが入っており、マウスオーバーで末尾に付いたブログ名なども確認出来た。これも復活させて欲しい。(前回変更以前のように、全部表示してくれるとさらに良いけど。)
良くなったところは、日付が時分まで表示されるようになったこと。何時ぐらいに最初にブックマークされたかを見るのに、今まではエントリページを開いて最終更新時間を見ないと行けなかったので、かなり便利になった。
スマートフォンでの表示
スマートフォンを意識したデザインに見えて、スマートフォンでもやっぱり使いにくい。
(特に自分はWebブラウザは横画面で見てるので)
と思ってたんだけど、スマートフォン横持ちで使うのに適した表示モードあったよ。
http://b.hatena.ne.jp/entrylist?url=.&threshold=3
サイトごとの新着エントリ用のページみたいだけど、「url=.」を指定すれば通常の(全サイトの)新着エントリが見える。
このURLでアクセスして、1件目をダブルタップすれば、1エントリが丁度一画面で表示され、あとは縦にスクロールしていけばスルスル見ていける感じになるので、流し見に最適だ。画像も右側に出るだけなので必要な情報だけ追いやすい。
でもやっぱり次ページリンクが不便なので、オートページャーは入れて欲しい。
あと自分は「n users」のところを長押しして別ウィンドウでエントリページを開くことが多いので、文字のあるとこだけじゃなくて右端までリンクになってるとうれしいかも。
まとめ
- オートページャー復活 or 次リンクにキーボードショートカット
- タイルの中身の順番はブックマーク数/カテゴリ&日付/タイトル/サイト情報/タグ/画像/サマリ
- B!ボタンを付ける
にしてくれれば、今のLook & Feelを残したまま結構快適に使えると思う。
単純に元に戻すのは嫌だな。進歩が無い。サイトが格好いいのは良いことだ。問題なのはそれが全く機能性に結びついてないことで、言ってみればモックと同じだ。やるんなら、格好よくて機能的なのがいい。
Play2.0で日付入力部品を作る
"Play 2.0"もとい"Playframewok 2.0"もとい"Play framework 2.0"で*1、既存のコンポーネントを組み合わせて、新しいコンポーネントを作りたい。
例えば、日付を入力するのに、セレクトボックスで「年」「月」「日」を選んで入力するフォームを考える。
こういうのは結構頻繁に再利用するので、再利用可能な部品として作っておきたい。
日付用のMapperの定義
「名前」と「誕生日」をフィールドとして持つ「人物」クラスを考える。
import java.util.Date case class Person(name: String, birthday: Date)
これを入力するためのフォームを定義する。例えばこんな感じ
val personForm = Form( mapping( "name" -> nonEmptyText, "birthday" -> date )(Person.apply)(Person.unapply) )
dateはplay.api.data.Forms._で定義されているMapping[java.util.Date]のインスタンス。
val date: Mapping[java.util.Date] = of[java.util.Date]
これはFormatter[java.util.Date]をimplicitに要求していて、その実体はplay.api.data.format.Formatで定義されている。内容を見ると、"yyyy-MM-dd"の書式で文字列とDateの変換をしていた。つまり、入力フォームに「yyyy-MM-dd」で入力してもらうような用途を想定しているようだ。
そうではなく、3フィールドに「年」「月」「日」を入力したら、日付として受け取れるようにしたい。
フォームのmappingは入れ子に出来るようなので、こういう風に書ける
val personForm = Form( mapping( "name" -> nonEmptyText, "birthday" -> mapping( "year" -> number, "month" -> number, "day" -> number )(applyDate)(unapplyDate) )(Person.apply)(Person.unapply))
applyDateとunapplyDateは年,月,日の数値の組とDateを変換する関数で、下のような感じで用意しておく。
import org.joda.time._ import java.util.Date // Int,Int,Int からDateを生成する def applyDate(year: Int, month: Int, day: Int) = new DateMidnight(year, month, day) toDate // DateからOption[(Int,Int,Int)]を生成する def unapplyDate(data: Date) = { val datetime = new DateTime(data) Some(datetime.getYear, datetime.getMonthOfYear, datetime.getDayOfMonth) }
これで、personForm("birthday.year")、personForm("birthday.month")、personForm("birthday.day")のようにFieldを引き出してHTML上でフォーム部品を組み立てられる。
mappingを毎回書くのは面倒なので、切り出して定義しておく
package data.format import play.api.data._ import play.api.data.Forms._ import org.joda.time._ import java.util.Date // ※注意このMappingは例外が起こるんで安全ではないよ object DateComponentMapping { // Int,Int,Int からDateを生成する def applyDate(year: Int, month: Int, day: Int) = new DateMidnight(year, month, day) toDate // DateTimeからOption[(Int,Int,Int)]を生成する def unapplyDate(data: Date) = { val datetime = new DateTime(data) Some(datetime.getYear, datetime.getMonthOfYear, datetime.getDayOfMonth) } // 年、月、日で構成される日付フォーム def dateComponent: Mapping[Date] = mapping( "year" -> number, "month" -> number, "day" -> number )(applyDate)(unapplyDate) }
使う側:
import data.format.DateComponentMapping.dateComponent val personForm = Form( mapping( "name" -> nonEmptyText, "birthday" -> dateComponent )(Person.apply)(Person.unapply) )
これでAction側では細部のフィールド構成を特に意識せず入力をDateオブジェクトで受け取れるようになった。
日付入力部品のHTMLの定義
日付用のMappingが準備できたので、それに対応する形で再利用可能なHTML部品を作りたい。
具体的には、上記のdateComponentが設定されたフィールドを受け取って、その内部の「年」「月」「日」の3つのフィールドをプルダウン3個に結びつけるようなHTMLテンプレートを書く。
app/views/helper/dateselector.scala.html:
@(field: play.api.data.Field, args: (Symbol,Any)*)(implicit handler: FieldConstructor, lang: play.api.i18n.Lang) @defining( (1900 to 2020).map(_ toString).map(s => (s,s)), (1 to 12).map(_ toString).map(s => (s,s)), (1 to 31).map(_ toString).map(s => (s,s)) ) { case(years, months, days) => @select(field("year"), years, args:_*) @select(field("month"), months, args:_*) @select(field("day"), days, args:_*) }
ちょっと手抜きで年は1900〜2020の固定にしてるけど、ここはパラメータで指定できても良いかも。
個々のフィールドは、親のフィールドに対してfield("year")のようにして取り出すことができる。
呼び出す側はform("birthday")などのように親のフィールドだけ渡してやればよい。
@(person: Form[Person], message: String) @import helper._ ... @form(routes.Sample.update) { @inputText(person("name")) @dateselector(person("birthday")) <input type="submit" value="登録"> }
まあエラー表示をちゃんとしたりselectを横並びにしたかったりした場合はHTML側でもう少し整理が必要と思うけど、原理的にはこれで可能なことが分かる。
こうやって作った部品をさらに組み合わせて、例えば日付の範囲を入力するための部品を作ったりもできるはず。
入力チェックについて
ドキュメントを見ると、ad-hocな制約を掛ける例がtuple用のものしかなく、mappingで定義した場合にどうやるか分からなかった。
一つの方法としては、tupleで作ったあとtransform()で目的の型に変換するように書けばいけるかも。
mappingのままやる場合、何が難しいかと言うと、子のフィールドでintに変換した後、元のmappingのapplyが呼ばれる間にverifyが設定できないのが困る。
仕方ないので、applyで例外が投げられたら拾ってFormErrorで返すようにしてみた。
// 恒等関数 def id[A](a: A) = a // mapping()で作ったMappingでapply時に例外が発生するようなものはad-hoc constraintsが掛けられないので、ラッパーを作成 def safeMapping[A](wrapped: Mapping[A], e: (String, Throwable) => Seq[FormError]): WrappedMapping[A, A] = new WrappedMapping(wrapped, id[A], id[A]) { override def bind(data: Map[String, String]): Either[Seq[FormError], A] = allCatch.either(super.bind(data)).left.map(e(this.key, _)).joinRight override def withPrefix(prefix: String): Mapping[A] = safeMapping(wrapped.withPrefix(prefix), e) } // 年、月、日で構成される日付フォーム def dateComponent: Mapping[Date] = safeMapping( mapping("year" -> number, "month" -> number, "day" -> number)(applyDate)(unapplyDate), { case (key, e) => Seq(FormError(key, "error.date", Nil)) } )
WrappedMappingの実装に依存した感じであんまりちゃんとした実装じゃないけど。
感想
Play2.0では、フォームを処理するのにMappingをうまく部品化すれば、それらを組み合わせることで完結&柔軟に複雑なフォームを組み立てていけることが分かった。
また、テンプレートエンジン側でも再利用可能な部品を組み合わせて複雑なフォームを組み立てていけることが分かった。
でも、ひとつの複雑なフォームを作るのに、Form側とHTMLテンプレート側で部品をペアで用意して、かつ、使用するときには別々にツリーを作っていかなくてはいけない。これはDRYじゃないと思う。
例えば、JavaServer Faces(というかTapestry(とういかWebObjects))のように、HTMLテンプレート側でコンポーネントツリーを一回作ったら、リクエスト時とレスポンス時の両方でそれを使えるようになってればいいと思うんだよね。
言い換えれば、一つ一つの部品がビュー的な部分(表示)とコントローラ的な部分(入力)をセットで持っていて、そういうプラガブルな部品を組み立ててビュー全体とコントローラ全体を作れるようになっていると便利だと思う。
なんでそうなってないんだろう。なんで15年前(WebObjects)は出来ていたことが今できてないんだろう。
*1:検索しにくいんだよksg