Scalaで#map系メソッドで副作用を起こすとバグるやつ

遅延評価と副作用は相性悪いよねって話です。

Scalaでforeachではなくmapの中で副作用を起こすとたまに評価タイミングによるわかりづらいバグに遭遇することがあります。

次のコードはMap#mapValuesの中でscalikejdbcで書き込みを行おうとするコードです。 一見うまくいくようで、エラーになります。手元では java.sql.SQLException: Connection is null. というエラーが発生しています。

import scalikejdbc._

val contents = DB.localTx { implicit session =>
  data.mapValues { s =>
    val text = s * 2
    sql"insert into test_table(text) values ($text)".update().apply()
    text
  }
}

contents.foreach(println)

mapValuesに渡している関数の処理はcontentsが評価されるまで行われません。つまり contents.foreach(println) の処理が行われるタイミングでDBへの書き込みを実行するのでその時にはトランザクションがcommitされてしまっているのです。

ちなみにこれはあくまでmapValuesの実装がそうなっているからで、mapでは起きませんでした。そんな実装依存でいいのかって話ですが、そんなこと言うと副作用はダメだ、参照透過なら何も問題ないだろ!と怒られます。怖いですね。

これは .view.force とやってその場で評価させるとうまくいっちゃいます。

import scalikejdbc._

val contents = DB.localTx { implicit session =>
  data.mapValues { s =>
    val text = s * 2
    sql"insert into test_table(text) values ($text)".update().apply()
    text
  }.view.force
}

contents.foreach(println)

いや、なんかひどいですねこれ。.view.force ?意味ないじゃん、と思って消すとバグります。 やっぱり普通に副作用は分けて、mapじゃなくてforeachにしましょう。

val texts = data.mapValues { s => s * 2 }
DB.localTx { implicit session =>
  texts.foreach { case (k, v) =>
    sql"insert into test_table(text) values ($v)".update().apply()
  }
}