kotlin における検査例外的なものについて

ふと思ったのでメモ。

おいらは java では検査例外を結構気に入って使っていたが、kotlin では実行時例外しかないので微妙に困った。しかし、そもそも検査例外に満足していたわけでもない。

基本的にはインタフェースメソッドの仕様はユースケースに近い考え方で、主経路の結果が戻り値、代替経路の結果が例外というような考え方をしている。しかしこれは、様々な側面を考慮したうえで java 言語仕様に合わせた結果こうなったというだけの話で、理想形と言うわけではない。という感じで、いくつか思うところがある:

  • 例外を代替経路の結果としてとらえた場合、常に例外の型だけで代替経路を識別できるわけではない。複数の代替経路で同じ型の値を返したい場合には、代替経路の種類と値の両方の識別が必要となる。kotlin の場合、例外を使わず sealed class で対応し、sealed class の型を経路識別に利用し、経路の網羅性をコンパイラにチェックされるというのが落としどころですかね。
  • 複数の戻り値を得たほうが便利なこともある。純粋関数以外を認めないというのであればそれもいいが、関数は処理の段階的詳細化でも利用される。その際、戻り値が1つだとめんどくさいこともある。例えば x, y, z という int 値を受け取りたい場合、ポインタ渡しのほうがラクチンというようなケースもある。実際はポインタはバグを生みやすいので、kotlin なら destructuring でそれっぽい感じにするというのが落としどころな気はする。val (x, y, z) = hoge() 的な感じで。*1
  • 各経路用のクロージャを渡すという方法も無くはない。副作用のみを期待する場合には実は結構きれいにコーディングできる、、、のだがそれが罠。クロージャ渡しをしたくなる誘惑は主に呼び出し側と呼ばれる側の両方のコードが頭に入っている場合に発生する。例えば、関数Aが関数Bを呼び出し、関数Bの代替経路XとYで個別に処理する場合、代替経路の結果がXであるかYであるかの条件分岐を行ってから処理を行う必要がある。しかし、クロージャを渡してしまえば関数A側は処理を渡してそれでおしまいになる。しかし、クロージャが呼ばれたかどうかの保証は得られないし、クロージャの処理の結果に応じて行うべき処理を後付けしようとしたらクロージャから値を入れるための変数を事前に作るなどの処理が必要となり、コードがかなりカオスになってくる。また、クロージャの処理が実行されるタイミングは関連コードを全てホワイトボックスで同時に把握しない限り判断できないため、クロージャが呼び出されるタイミングが問題になった時に、関連コードすべての coupling が成立してしまう。とかいろいろ問題があるわけだが、コードの実行を時系列で追いづらくなるクロージャは無条件で複雑さを導入するのでそれ以上のメリットが無い場所ではやるべきじゃないと思われ。

とりあえずの結論:

  • 関数を代替経路のあるユースケースに見立てる場合は、例外は使わず sealed class を使い、経路毎に異なる型を割り当てる。*2
  • 関数の戻り値を複数得たい場合や分解して得たい場合は destructuring を利用する。*3
  • 戻り値を判断して対応可能な処理はクロージャ渡しでやらない。

という感じですかね。

*1:hoge()側の戻り値の型をどうするのかはプロジェクトのポリシー次第でしょうね。data class Point3D(val x: Int, val y: Int, val z: Int) のようなクラスを作るのか、Triple<Int, Int, Int> のようにカスタムクラスは作らないで対応するのかなど。可読性や将来的な変更局所性はカスタムクラスのほうが高いと思われる。

*2:when による条件分岐では when を値として利用しない場合も val dummy:Unit = when のように変数に代入して、sealed class の型を網羅していない場合はコンパイルエラーになるようにする方法が結構使える。

*3: val (x, y, z) = hoge() のような感じ。