ファーストクラスコレクションのScala実装

2 min read読了の目安(約2300字

ファーストクラスコレクションとは?

ファーストクラスコレクションは、言語のプリミティブなコレクションをラップしてコレクションの機能を拡張する実装パターンです。
以下の例は、商品のコレクションをラップして価格の合計と税額を求める計算を拡張機能として提供し、ロジックを再利用できるようにします。

class Items(values: Seq[Item]) {

  def total: BigDecimal = values.map(_.price).sum
  
  def tax: BigDecimal = total * 0.08
  
}

問題点

以下のようなコードはデメテルの法則に違反しているという議論が発生しえます

val pItems: Seq[Item] = ???
val items = new Items(pItem)
for {
  item <- items.values.filter(_.price > 100) // dotが2つ
  ...
} ...

これを回避するために、以下のようなカプセル化が行われるかもしれません

// オリジナルのコレクションをprivateにして隠蔽
class Items(private values: Seq[Item]) {

  def total: BigDecimal = values.map(_.price).sum
  
  def tax: BigDecimal = total * 0.08
  
  // filterはラップする
  def filter(f: Item => Boolean): Seq[Item] = values.filter(f)
  
  // 元のコレクションも取得できるようにしておく
  def asSeq: Seq[Item] = values
}

しかし、filterの実装があまりにも自明だし元のコレクションを戻せるようにしているのは public なままの場合とどう違うんだという話もあり、スッキリしない方もいるんじゃないでしょうか?

暗黙変換(implicit conversion)を使う

先述したように、ファーストクラスコレクションを実装する目的は「言語のプリミティブなコレクションをラップしてコレクションの機能を拡張する」ということですが、これを実現するためにラッパークラスという形をとるのは、Javaなどの言語の場合、言語のプリミティブなコレクションの拡張ができないためでしょう。しかし、実装言語によっては標準ライブラリや3rdパーティ製のクラスを拡張する機能が言語に備わっており、Scalaでは暗黙変換(implicit conversion)を利用することでこれを実現できます。
また、Javaなどでは標準のコレクションは可変(mutable)ですが、Scalaのコレクションは不変(immutable)であるため、Collections.unmodifiableListといったメソッドを利用してファーストクラスコレクション内部のコレクションが破壊されないよう保護するためにカプセル化を行うモチベーションはScalaの実装では元々少ないと思われます。

// 暗黙クラスの宣言
implicit class Items(values: Seq[Item]) {

  def total: BigDecimal = values.map(_.price).sum
  
  def tax: BigDecimal = total * 0.08
  
}

このクラスを利用するコードは以下のようになります。

import Items

val items: Seq[Item] = ???
val price = items.filter(_.price > 100).total
val tax = items.tax

filterは元のコレクションの関数を使用していますが、既存のプロジェクトにファーストクラスコレクションという概念を導入する場合、こうした実装やファーストクラスコレクションにラップされる対象となるコレクションをメンバーに持つクラスの実装を変更せずに済むのもこの方式の大きなメリットの1つです。

暗黙変換を利用する場合のデメリット

暗黙変換を利用する場合は以下のようなデメリットもあります。

  1. コンパイル時間と実行時オーバーヘッド
    一般的に暗黙クラスに宣言したメソッド呼び出しがあると、コンパイル時のオーバーヘッドとなりコンパイル時間が増加してしまう可能性があります。また、暗黙クラスに宣言したメソッドの呼び出しは、実行時に暗黙クラスをnewした上で実行されるため、実行時にもオーバーヘッドがあります。つまり、暗黙変換を利用した抽象化はコストがかかります。
  2. 密結合化
    暗黙クラスを利用した実装では、拡張した機能の呼び出しごとに暗黙クラスがnewされるため、ファーストクラスコレクションの型を持ちまわす場合と比べると密結合となり、拡張した機能のモック化などは難しくなります。

参考文献