Edited at

map が単射ではなかった件

More than 3 years have passed since last update.

今日遭遇したバグ。簡略化して書くと以下のような処理だった。

trait X {

def foo():Iterable[String]
}
class Y extends X {
def foo() = Seq("ab", "c", "def", "ghi")
}
class Z extends X {
def foo() = Set("ab", "c", "def", "ghi")
}

new Y().foo().map{ _.length }.sum
new Z().foo().map{ _.length }.sum

どちらも a~i までの 9 文字を数えているだけなので同じ 9 という結果になると思いきや Z の方が小さい結果になります。

scala> new Y().foo().map{ _.length }.sum

res1: Int = 9

scala> new Z().foo().map{ _.length }.sum
res2: Int = 6

Z は重複した値を排除する目的で内部では Set を使用していたわけですが、その map 結果も Set となるため "def" 要素と "ghi" 要素に対応する _.length=3 が重複排除され結果として Y より 3 少ない値となったわけです。Set を選択している場所は多態化した特定のクラスのみであり map を使用している場所とは離れています。ぱっとコードを見ただけで原因に気づくのは難易度が高い。実際、ステップ実行してみてインスタンスが Z の場合だけ foo()Set を返していることを見つけて気づいたわけです。

map は単射 (前後で要素数が変わらない) と認識していたわけですが、単射とも限らず元の集合の実装特性による動きになるのか、値域に同値があるからそもそも単射じゃないと解釈するのか、まぁどちらにせよもやっと微妙な感じが残りました。