プロジェクトで一つの書き方に統一したくて悩んでいる。ちなみにまだどれも実際に使ったことがないので下の感想は想像レベル。
Future[Option[...]]派
良いところ
- 同期バージョンの型をOption[...]にしていたらFuture[Option[...]]にするのは比較的考えることが少なそう
- 複数取得するときの型をFuture[List[...]]にするとmapとかの見た目上対称性のあるコードになりそう(?)
悪いところ
- コードが煩雑になりがち
- 2つの型クラスに包まれているとforを使いにくい
repository.resolveAsync(id).map {
maybeRecord => maybeRecord.map { ... }
}
Future[Either[...]]派
- play2とか使ってると、Futureがよく出てくる
- Futureをそこら中でAwaitしたらFuture使う意味がないので、Future[A]をmapやflatMapなどでどんどん連鎖させて、メソッドの型にFutureが大量に出現!
- Futureは非同期に行われる処理であり、そこで例外発生したら、その中に例外を含むしかない(単純に例外投げるわけにはいかないというか不可能)
- Scala標準ライブラリのFutureは、Throwable型で例外を保持できるようになってる
- 逆にいうと、Throwableでしか保持できない
- 例外の種類が多くなってきて、プログラムが複雑になった場合、Throwableではなくもっと限定された独自のエラー型で表現したい
- すると Future[ Either[エラーの型, 結果型] ] にしたくなる
- しかし、それはそれで、Future と Either が混ざった型をうまく扱う方法がわからずに死
良いところ
- 「例外の種類が多くなってきて、プログラムが複雑になった場合、Throwableではなくもっと限定された独自のエラー型で表現したい」というときに便利なのかも?
悪いところ
- だいぶ複雑になりそう
- モナドトランスフォーマー?
- scalazに依存したくないし、かと言ってモナドトランスフォーマーを自分で定義すると初心者お断りコードになりそう
単なるFuture[...]派
リポジトリには、次のような集約ルートであるエンティティ(以下 ルートエンティティ)を永続化(保存したり読み込んだり)するI/Fを定義します(Mには、同期型I/Oならscala.util.Try、非同期型I/Oならscala.concurrent.Futureを指定することができます)。それらは、ルートエンティティを”保存する(save/store)”もしくは”識別子で解決する/読み込む”などの永続化の言葉に対応します。SQLのinsert,updateより抽象度が高い言葉です。
trait Repository[M[+ _], ID <: Identity[_], E <: Entity[ID]] {
type This <: Repository[M, ID, E]
// 識別子を指定してエンティティへの参照を取得する
def resolve(identity: ID)(implicit ctx: EntityIOContext): M[E]
// エンティティを保存する
def store(entity: E)(implicit ctx: EntityIOContext): M[(This, E)]
// 識別子を指定してエンティティを削除する
def delete(identity: ID)(implicit ctx: EntityIOContext): M[(This, E)]
}
良いところ
- mapとかの数が減ってコードの見通しが良くなりそう
- 最初から同期版がOptionではなくTryにしていた場合はFutureにするのは手間がかからない
悪いところ
- Optionと違って、取得対象のレコードが存在しなかったことを表すのに独自の例外を定義しないといけない
- それ以外の、例えばDBに接続できなかった例外などは別にハンドリングせずにユーザーにエラーを見せても良いというスタンスの場合、これだけのために例外を一つ定義するのが微妙な気がする
所感
どれを選ぶかによってコードがだいぶ変わってくるので、あとで別のに変えるのが大変そう。
同期のところを非同期にするのにもだいぶ大幅な書き換えになるので、またやるのはつらい。
もしかしてほんとうに欲しかったものはFutureではなくてC#のasync/awaitだったのではという気もする。
(とは言えコールバッククラスのインスタンスを渡したりするのよりはFutureのほうがよっぽどマシだけど)
定番があるなら合わせたい。