※ 2017/2/16 24:00 サンプルコードをより意図が分かりやすいと思われるコードに変更しました。 元の文章は後ろの方に置いてあります。
こんにちは、杉谷と申します。
ちょと前までエグゼクティブエンジニアというよく分からない肩書きだったのですが、よく分からなくて不便なのでCTOと名乗ることになりました。 やっていることは変わらず、コミックスマート株式会社*1でGANMA!を開発しつつ、セプテーニ・オリジナル株式会社*2でこんなことや こんなことをやっています。引き続きよろしくお願いいたします。
Future[Unit] の罠
みなさま副作用はあるけど非同期なメソッドってdef hoge:Future[Unit]
と書きたくなりませんか?
結構書きたくなる場面があるのですがFuture[Unit]には うっかり.mapで繋げてしまうと意図しない順番で動作する、という罠がありました。
object TrapExample { def funcA(): Future[Unit] = Future { println("a") } def funcB(): Future[Unit] = Future { Thread.sleep(100) println("b") } def funcC(): Future[Unit] = Future { println("c") } def A_B_Cのつもりで書いた間違っているのにビルドが通る例(): Future[Unit] = { funcA() .map { _ =>funcB()} .map { _ =>funcC()} // ※たとえでこう書いていますが、本来はfor文とかを使いましょう // ※返値の型は本当はFuture[Future[Unit]]です。 } def A_B_Cにするつもりなら動く例(): Future[Unit] = { funcA() .flatMap { _ =>funcB()} .flatMap { _ =>funcC()} } }
A_B_Cのつもりで書いた間違っているのにビルドが通る例()
を実行すると
a c b
になります。
これはUnit型の注釈がついた値や式において、本来返されるはずのUnit型ではない値は返さずに捨てられる、という特別ルールにより発生する仕様です。
逆に、これはUnit以外にすればコンパイルは通らないので、誤用に対して多少安全になります。
object SafeExample { case class Hoge() def funcA(): Future[Hoge] = Future { println("a") Hoge() } def funcB(): Future[Hoge] = Future { Thread.sleep(100) println("b") Hoge() } def funcC(): Future[Hoge] = Future { println("c") Hoge() } /* def もうビルドはとおらない(): Future[Hoge] = { funcA() .map { _ =>funcB()} .map { _ =>funcC()} } */ }
実際にはまったコードは以下のような雰囲気でした
// 消してから何かを再計算、のつもりが再計算してから消すになっている、的なことを伝えたいコード def delete(hogeId: HogeId):Future[Unit] = { hogeAsyncRepository .getBy(hogeId) .map{ case Some(hoge) => hogeAsyncRepository.delete(hogeId) case _ => Future.successful(Unit) } .map { hogeScoreService.recalc() } }
回避策
“空"を表す型を作る
型があれば大丈夫なのであれば、空を表す新しい型を定義してしまえばよい、というのが最初に思いつきました。
case class VoidResult() object SafeExample { def funcA(): Future[VoidResult] = Future { println("a") VoidResult() } def funcB(): Future[VoidResult] = Future { Thread.sleep(100) println("b") VoidResult() } def funcC(): Future[VoidResult] = Future { println("c") VoidResult() } }
この方式は
- いろんなところで似たような実装が発生しそうで危なそう
- あちらこちらで明示的に
[return] VoidReult()
と書かなくてはいけない箇所がでて格好悪い
という欠点があるので、ないよねーとなりました。
コンパイラオプション
コンパイルオプション -Ywarn-value-discard
を付けるとコンパイラが警告をしてくれるようになります。
$ scalac FutureUnit.scala -Ywarn-value-discard 〜 FutureUnit.scala:16: warning: discarded non-Unit value funcB().map { b => ^ four warnings found
ただし副作用として
def hoge : Unit = { getString() // : String }
なコードも
def hoge : Unit = { getString() // : String () }
こう書かないとワーニングがでてしまうようになります。
最初からこのオプションを有効にしていれば有りかも、な対策です。
Future[Unit]が駄目なわけではない
弊社技術顧問のお一人である麻植さん曰く
taisukeoe [1:25 AM]
ちょっと乗り遅れ気味ですが、興味深い議論なので。 > Future[Unit]ざくっと調査した感じ、Future[Unit] 自体は割と広くScala界(ってなにかはさておき)では受け入れられてそうです。
回答者のTravis Brownはcirceの作者だったり、Scalaz Contributorだったけど今は猫派の急先鋒だったりするStackOverFlow Godです。
https://github.com/travisbrown?tab=repositories
上のStackOverFlowの回答内でもありますが、TwitterのライブラリにもFuture[Unit]ありますね
https://github.com/twitter/util/blob/master/util-core/src/main/scala/com/twitter/util/Closable.scala
Future[Unit]は仰るとおりの危険性はありますが、それを言うとM[Unit] m:Monadな型は同じ問題があるといえばあるので、局所的に大きな問題の発生するケースでのみVoidResultみたいな特殊な型でというのも選択肢にはありえそうですが、大域的には @*** さんの仰る通りコンパイラオプションやlintツールでカバーするのが現実的な気がしております。
しかし、Monad合成のbind operator >>= が欲しくなりますね… :)
とのこと。
結局の所、この件はテストをちゃんと書けば検出できるので GANMA!ではコンパイラオプションもVoidResultも使わず、気をつけながらそのまま使う、となりました。
終わりに
如何でしたでしょうか? できれば普通にandThenを使っていきたいところですね!
セプテーニ・オリジナルでは、いつでもScalaをやってみたい方を募集しております。
我々はScalaを採用し、ドメイン駆動設計(DDD)で開発をしている企業です。 最近入社される方は、次のような方が多いです。
セプテーニ・オリジナルではScalaやDDD未経験でも問題ありません。 入社された方のために以下のカリキュラムを通してScala・DDDのトレーニングを積むことが出来ます。
レビュアーに選出された5名がつくので、適宜アドバイスを受けながら進められます。
最近は他社様とのコラボ企画が多かったので、間が空いていますが 勉強会イベント http://septeni-scala.connpass.com/ やもくもく会 https://sep-ori-mokumoku-day.connpass.com/ も不定期に行っています。 ご興味をもたれた場合、connpassにご登録の上、上記グループにご参加していただくとイベント予定の通知が届き便利です。
もし我々に興味をもっていただけたのであれば http://septeni-original.co.jp よりエントリーいただだけると幸いです。
社内見学イベント http://tour.septeni.net/ も毎月実施しています。 まずは様子を見たい、というときはこちらもご検討ください。
以上です。読んで戴きましてありがとうございました!
編集の記録 - 2/16 24:00変更前の冒頭
以下の文章は書き換え前の冒頭の文章です。 実行順の事は気にせず、コンパイルが通ってしまうよ、としか言っていないコードだったので変更しました。
object TrapExample { def funcA(): Future[Unit] = Future.successful(()) def funcB(): Future[Unit] = Future.successful(()) def funcC(): Future[Unit] = Future.successful(()) def A_B_CのつもりならflatMapを使うべきなのにビルドが通る例(): Future[Unit] = { funcA().map { a => funcB().map { b => funcC() } } } // ※この場合、本来はfor文とかandThenを使うのが正解 }
これはUnitでなければコンパイルが通らないので安全です
object SafeExample { case class Hoge() def funcA(): Future[Hoge] = Future.successful(Hoge()) def funcB(): Future[Hoge] = Future.successful(Hoge()) def funcC(): Future[Hoge] = Future.successful(Hoge()) /* def コンパイルエラー(): Future[Hoge] = { funcA.map { a => funcB.map { b => funcC } } } */ def 通る(): Future[Hoge] = { funcA().flatMap { a => funcB().flatMap { b => funcC() } } // ※本来はfor文とかandThenを使おう!! }