implicit class を利用して Seq (とか)にメソッドを生やす

case classSeq に対するメソッドを生やしたい時どうするか。例えば、下記のような case class があるとする。

case class Range(
    min: Int,
    max: Int
)

これの Seq つまり、Seq[Range] に対して、含まれるRangeの中から、最小の min と 最大の max を取得し、それを Range として欲しい時どうするか。利用箇所が1箇所なら、普通に map とかで頑張って処理しちゃえばいい気もするが、複数箇所で利用したり、そもそもコードがごちゃつくので少し避けたい。かといって、下記のように、それ用のメソッドを用意するのもやりすぎ感ある。

object Range {
  def minMaxRange(values: Seq[Range]): Range = {
    val min = values.map(_.min).min
    val max = values.map(_.max).max

    Range(min, max)
  }
}
val ranges = Seq(
  Range(1, 10),
  Range(2, 9),
  Range(0, 3),
  Range(8, 11)
)

Range.minMaxRange(ranges)</pre>

呼び出し元も、少し長くなって読みづらい。

結論

case class のコンパニオンオブジェクトに implicit class を作成して使う。

case class Range(
    min: Int,
    max: Int
)

object Range {
  implicit class RangesOps(values: Seq[Range]) {
    def minMaxRange: Range = {
      require(values.nonEmpty)
      val min = values.map(_.min).min
      val max = values.map(_.max).max
      Range(min, max)
    }
  }
}

こうすることで、Seqに対して定義済みの値があるように使え、呼び出しもとが簡潔になる。

val ranges = Seq(
  Range(1, 10),
  Range(2, 9),
  Range(0, 3),
  Range(8, 11)
)

ranges.minMaxRange   // val res0: Range = Range(0,11)

PreScalaMatsuri で似たような発表があったけど、その時はWrapperクラスを用意する方針の話だったかな。。?個人的には無駄で意味のないクラスはできる限り増やしたくないので、現状は implicit class の利用がいいかなと思ってる。

追記

ラッパークラスを用意する方針は、「コレクションオブジェクト」あるいは「ファーストクラスコレクション」というテクニックに該当することを後で知りました。 ちょっとしたコレクション処理の追加ならばコンパニオンオブジェクトへの implicit class の追記。コレクションに対して制約がある場合などはコレクションオブジェクトを作るのがいいかなというのが最近の私の方針です。