新しい集合やマップの参入 トップ RNA メソッドの戻り値型の適応 map 類の取り扱い 目次

map 類の取り扱い

しかし、まだ取り扱っていないコレクションのメソッドの種類が一つある。これらのメソッドは、はっきりとしたコレクションの型を返さない。同じ種類のコレクションを返すかもしれないが、要素型が異なったりする。この古典的な例が map メソッドだ。もし sSeq[Int] で、fInt から String への関数である場合、s.map(f)Seq[String] を返す。 つまり、要素型がレシーバから戻り値へと変わるが、コレクションの種類は変わらない。

他にも map のように振る舞うメソッドはいくつもある。(flatMapcollect のように)それが明らかなメソッドもあるが、他のものは予想外かもしれない。例えば、追加のメソッド ++ も引数とは別の型の戻り値を返す可能性がある(Int のリストに String を追加すると Any のリストを返す)。これらのメソッドは RNA鎖にどう適応できるだろう。理想的には、RNA鎖について塩基から塩基へ写像した場合、再び RNA鎖が返ってくる:

scala> val rna = RNA(A, U, G, G, C)
rna: RNA = RNA(A, U, G, G, C)
  
scala> rna map { case A => C case b => b }
res7: RNA = RNA(C, U, G, G, T)

同様に、++ を用いて二つの RNA鎖を連結した場合、再び別の RNA鎖を返すべきだ:

scala> rna ++ rna
res8: RNA = RNA(A, U, G, G, C, A, U, G, G, C)

一方、RNA鎖について塩基を別の型に写像した場合は、新しい要素は違う型を持つので RNA鎖を返すことができない。代わりに、列を返さなくてはならない。同様に、RNA鎖に対して Base 型以外の要素を追加した場合は、RNA鎖ではなく、一般の列を返す。

scala> rna map Base.toInt
res2: IndexedSeq[Int] = Vector(03112)
  
scala> rna ++ List("missing""data")
res3: IndexedSeq[java.lang.Object
  Vector(A, U, G, G, C, missing, data)

上記は理想的な場合に期待されるものだが、RNA2 クラスが提供するものではない。事実、最初の二つの具体例をこのクラスで実行すると以下のような結果となる:

scala> val rna2 = RNA2(A, U, G, G, C)
rna2: RNA2 = RNA2(A, U, G, G, C)
  
scala> rna2 map { case A => C case b => b }
res0: IndexedSeq[Base] = Vector(C, U, G, G, C)
  
scala> rna2 ++ rna2
res1: IndexedSeq[Base] = Vector(A, U, G, G, C, A, U, G, G, C)

つまり、map++ の戻り値は、生成されるコレクションの要素の型が塩基であっても RNA鎖ではないということだ。改善するには、map メソッドのシグネチャを観察して損はない(もしくは同様のシグネチャを持つ ++)。map メソッドは scala.collection.TraversableLike クラスにて以下のシグネチャで定義されている:

def map[B, That](f: A => B)
  (implicit cbf: CanBuildFrom[Repr, B, That]): That

ここで、A はコレクションの要素の型で、Repr はコレクションそのものの型だ(これは、TraversableLikeIndexedSeqLike などの実装トレイトに二番目の型パラメータとして渡される)。map メソッドは、BThat というもう二つの型パラメータを取る。B パラメータは、写像関数の戻り値型を表し、これは新しいコレクションの要素型でもある。That は、map の戻り値型であるため、新たに作成されるコレクションの型を表す。

では、That 型はどのように決定されるのだろう。実は、これは CanBuildFrom[Repr, B, That] 型の暗黙のパラメータ cbf によって別の型に繋がっている。これらの CanBuildFrom の暗黙の値 (implicit value) は、それぞれのコレクションクラスにより定義されている。要約すると CanBuildFrom[From, Elem, To] 型の暗黙の値は、「From 型のコレクションがあるとき、こうすれば要素型 Elem を持つ To 型のコレクションを構築できる」と言っているのだ。

 

final class RNA private (val groups: Array[Int]val length: Int
  extends IndexedSeq[Base] with IndexedSeqLike[Base, RNA] {
  
  import RNA._
  
  // `IndexedSeq` の `newBuilder` の必須再実装
  override protected[thisdef newBuilder: Builder[Base, RNA] = 
    RNA.newBuilder
  
  // `IndexedSeq` の`apply` の必須実装
  def apply(idx: Int): Base = {
    if (idx < 0 || length <= idx)
      throw new IndexOutOfBoundsException
    Base.fromInt(groups(idx / N) >> (idx % N * S) & M)
  }
  
  // 効率を上げるための、 
  // 必須ではない foreach の再実装
  override def foreach[U](f: Base => U): Unit = {
    var i = 0
    var b = 0
    while (i < length) {
      b = if (i % N == 0) groups(i / N) else b >>> S
      f(Base.fromInt(b & M))
      i += 1
    }
  }
}
RNA鎖クラス、最終版

 

  object RNA {
  
    private val S = 2            // number of bits in group
    private val M = (1 << S) - 1 // bitmask to isolate a group
    private val N = 32 / S       // number of groups in an Int
  
    def fromSeq(buf: Seq[Base]): RNA = {
      val groups = new Array[Int]((buf.length + N - 1) / N)
      for (i <- 0 until buf.length)
        groups(i / N) |= Base.toInt(buf(i)) << (i % N * S)
      new RNA(groups, buf.length)
    }
  
    def apply(bases: Base*) = fromSeq(bases)
  
    def newBuilder: Builder[Base, RNA] = 
      new ArrayBuffer mapResult fromSeq
  
    implicit def canBuildFrom: CanBuildFrom[RNA, Base, RNA] = 
      new CanBuildFrom[RNA, Base, RNA] {
        def apply(): Builder[Base, RNA] = newBuilder
        def apply(from: RNA): Builder[Base, RNA] = newBuilder
      }
  }
RNA コンパニオンオブジェクト、最終版

これで、RNA2 シークエンスの map++ の振る舞いの謎が解けたと思う。RNA2 シークエンスを作成する CanBuildFrom インスタンスが無いので、継承された IndexedSeq トレイトのコンパニオンオブジェクトから次善の CanBuildFrom が見つかったのだ。この暗黙の値は IndexedSeq を作成し、rna2map を適用したときに返ったのがそれだということだ。

この欠点を補うためには、RNA クラスのコンパニオンオブジェクト内に CanBuildFrom インスタンスの暗黙の値を定義する必要がある。そのインスタンスは CanBuildFrom[RNA, Base, RNA] 型を持つべきだ。つまり、このインスタンスは「ある RNA鎖があり、新しい要素型が Base であれば、RNA鎖である別のコレクションを構築できる」と述べる。上記に RNA クラスそのコンパニオンオブジェクトの詳細を示した。RNA2 クラスと比べて二つの大きな違いがある。第一に、newBuilder の実装が RNA クラスからコンパニオンオブジェクトに移ったことだ。RNA クラスの newBuilder メソッドは単にこの定義に委譲する。第二に、RNA オブジェクト内に CanBuildFrom の暗黙の値がある。そのようなオブジェクトを作成するには、CanBuildFrom トレイト内の二つの apply メソッドを定義する必要がある。両方共 RNA コレクションのビルダを作成するが、異なる引数リストを持つ。apply() メソッドは単に正しい型の新たなビルダを作成する。 それに対して、apply(from) メソッドは、元のコレクションを引数として取る。これはビルダの戻り値の動的型をレシーバの動的型に合わせるのに有用となるかもしれない。RNA の場合は、RNA が final クラスであるため、静的型 RNA のレシーバは動的型も RNA であるため関係ない。それが apply(from) も単に newBuilder を呼び出すだけで引数を無視している理由だ。

これで終わりだ。RNA クラスの最終版は全てのコレクションメソッドを自然な型で実装する。この実装には「お約束事」が多少必要だった。要するに、newBuilder ファクトリと暗黙の値 canBuildFrom をどこに置けばいいのかを知る必要がある。利点としては、比較的少ないコードで多数のメソッドが自動的に定義されるということがある。また、多値演算である takedropmap、や ++ を呼ばないならば、RNA1 クラスのコードで示した実装で止めておいて、ここまで頑張ったことをしないという選択もある。

これまでの議論は、特定の型に従うメソッドを持つ新しい列を定義するのに必要な最小限のコードを中心に進めてきた。しかし、実際には新しい機能を追加したり、効率のために既存のメソッドをオーバライドしたいと思うかもしれない。具体例としては、RNA クラスのオーバーライドされた foreach メソッドがある。foreach メソッドは、コレクションに対するループを実装するのでただでも重要なメソッドだ。さらに、他の多くのメソッドは foreach に基づいて実装されている。そのため、このメソッドの最適化に労力を割くことは論理的であると言える。IndexedSeq内の foreach の標準実装は、単に apply を用いてコレクションの全ての i番目の要素を選択する(このとき i0 からコレクションの長さ - 1 の範囲)。つまり、標準実装は配列要素を選択し、パック化解除して塩基にするという作業を RNA鎖にある全ての要素に対して行うことになる。RNA クラスのオーバーライドする foreach はもう少し賢い。全ての配列の要素に対して、選択後直ちに要素に含まれる全ての塩基に渡された関数を適用する。そのため、配列の選択とパック化解除のための労力がかなり軽減される。

続いては、新しい集合やマップの参入


新しい集合やマップの参入 トップ RNA メソッドの戻り値型の適応 map 類の取り扱い 目次