Magnolia Tech

いつもコードのことばかり考えている人のために。

ScalaのmapValuesの挙動が2.13.0から変わっているので実装を調べてみた

scalaのmapValuesは、Mapのvalueだけにmapを適用したい時に使用するメソッドです(Mapとmapでちょっと分かりづらいですが)。

Scala 2.12.6で実行すると、以下のような結果になります。

scala> val characters = Map("Gandalf" -> "wizard", "Aragorn" -> "ranger")
characters: scala.collection.immutable.Map[String,String] = Map(Gandalf -> wizard, Aragorn -> ranger)

scala> val upper = characters.mapValues( x => x.toUpperCase )
upper: scala.collection.immutable.Map[String,String] = Map(Gandalf -> WIZARD, Aragorn -> RANGER)

まだ正式リリースされていないScala 2.13.0ですが、先日リリースされた2.13.0-M5という開発途中のバージョンで実行すると、以下のような結果になりました。

scala> val characters = Map("Gandalf" -> "wizard", "Aragorn" -> "ranger")
characters: scala.collection.immutable.Map[String,String] = Map(Gandalf -> wizard, Aragorn -> ranger)

scala> val upper = characters.mapValues( x => x.toUpperCase )
warning: there was one deprecation warning (since 2.13.0); for details, enable `:setting -deprecation' or `:replay -deprecation'
upper: scala.collection.MapView[String,String] = <function1>

何か明らかに挙動が変わっています。

何が変わったのか?

ふとこの挙動の変化についてツイートしたところ、@xuwei_kさんに丁寧に教えていただきました。

Scala 2.12.6における実装は以下の箇所の場所です(これも教えてもらった…ありがとうございます)。

github.com

mapValuesはMapを返すように定義されていますが、実態はMappedValuesという型のインスタンスを返しています。この時点ではインスタンスが作られるだけで実はmapが実行されていません。valuesを取り出すまで(getが実行されるまで)mapValuesの引数に渡された変換関数は実行されていない、つまり遅延評価されるということです。

この挙動が型から分からない、ということが課題になった、ということですね。確かにscaladocにも書かれていないし、型は普通のMapだし、全然分からないですね。

Scala 2.13.0-M5での実装

github.com

MapView.MapValuesという型のインスタンスを返しています。

MapViewの実装は以下の通りになっています。

github.com

先ほどの結果にキーを渡すと、変換されたvalueが得られました。

scala> upper("Gandalf")
res4: String = WIZARD

この変更はmapValuesだけでなく、filterにも適用されています。filterなんかはよく使われていると思うので、けっこう影響が大きいかもしれないですね。Scala 2.13.0へのアップデートの時には気をつけましょう。

Scala 2.13.0へアップグレードするとき

先ほどのmapValuesの件以外にも、網羅的にまとまっているマイグレーションガイドが有りますので、来たるべきScala 2.13.0リリースに向けて、こちらを見ておくと良いでしょう。

https://confadmin.trifork.com/dl/2018/GOTO_Berlin/Migrating_to_Scala_2.13.pdf