Dealing with map and friendsTopIntegrating new collectionsAdapting the result type of RNA methodsContents

Adapting the result type of RNA methods

Here are some more interactions with the RNA1 abstraction:

scala> rna1.length
res2: Int = 5
  
scala> rna1.last
res3: Base = T
  
scala> rna1.take(3)
res4: IndexedSeq[Base] = Vector(A, U, G)

The first two results are as expected, but the last result of taking the first three elements of rna1 might not be. In fact, you see a IndexedSeq[Base] as static result type and a Vector as the dynamic type of the result value. You might have expected to see an RNA1 value instead. But this is not possible because all that was done in class RNA1 was making RNA1 extend IndexedSeq. Class IndexedSeq, on the other hand, has a take method that returns an IndexedSeq, and that's implemented in terms of IndexedSeq's default implementation, Vector. So that's what you were seeing on the last line of the previous interaction.

 

  final class RNA2 private (
    val groups: Array[Int],
    val length: Int
  ) extends IndexedSeq[Base] with IndexedSeqLike[Base, RNA2] {
  
    import RNA2._
  
    override def newBuilder: Builder[Base, RNA2] = 
      new ArrayBuffer[Base] mapResult fromSeq
  
    def apply(idx: Int): Base = // as before
  }

RNA strands class, second version.

Now that you understand why things are the way they are, the next question should be what needs to be done to change them? One way to do this would be to override the take method in class RNA1, maybe like this:

def take(count: Int): RNA1 = RNA1.fromSeq(super.take(count))

This would do the job for take. But what about drop, or filter, or init? In fact there are over fifty methods on sequences that return again a sequence. For consistency, all of these would have to be overridden. This looks less and less like an attractive option. Fortunately, there is a much easier way to achieve the same effect. The RNA class needs to inherit not only from IndexedSeq, but also from its implementation trait IndexedSeqLike. This is shown in the above listing of class RNA2. The new implementation differs from the previous one in only two aspects. First, class RNA2 now also extends from IndexedSeqLike[Base, RNA2]. The IndexedSeqLike trait implements all concrete methods of IndexedSeq in an extensible way. For instance, the return type of methods like take, drop, filter, or init is the second type parameter passed to class IndexedSeqLike, i.e., in class RNA2 it is RNA2 itself.

To be able to do this, IndexedSeqLike bases itself on the newBuilder abstraction, which creates a builder of the right kind. Subclasses of trait IndexedSeqLike have to override newBuilder to return collections of their own kind. In class RNA2, the newBuilder method returns a builder of type Builder[Base, RNA2].

To construct this builder, it first creates an ArrayBuffer, which itself is a Builder[Base, ArrayBuffer]. It then transforms the ArrayBuffer builder by calling its mapResult method to an RNA2 builder. The mapResult method expects a transformation function from ArrayBuffer to RNA2 as its parameter. The function given is simply RNA2.fromSeq, which converts an arbitrary base sequence to an RNA2 value (recall that an array buffer is a kind of sequence, so RNA2.fromSeq can be applied to it).

If you had left out the newBuilder definition, you would have gotten an error message like the following:

RNA2.scala:5: error: overriding method newBuilder in trait
TraversableLike of type => scala.collection.mutable.Builder[Base,RNA2];
 method newBuilder in trait GenericTraversableTemplate of type
 => scala.collection.mutable.Builder[Base,IndexedSeq[Base]] has
 incompatible type
class RNA2 private (val groups: Array[Int], val length: Int) 
      ^
one error found

The error message is quite long and complicated, which reflects the intricate way the collection libraries are put together. It's best to ignore the information about where the methods come from, because in this case it detracts more than it helps. What remains is that a method newBuilder with result type Builder[Base, RNA2] needed to be defined, but a method newBuilder with result type Builder[Base,IndexedSeq[Base]] was found. The latter does not override the former. The first method, whose result type is Builder[Base, RNA2], is an abstract method that got instantiated at this type in class RNA2 by passing the RNA2 type parameter to IndexedSeqLike. The second method, of result type Builder[Base,IndexedSeq[Base]], is what's provided by the inherited IndexedSeq class. In other words, the RNA2 class is invalid without a definition of newBuilder with the first result type.

With the refined implementation of the RNA2 class, methods like take, drop, or filter work now as expected:

scala> val rna2 = RNA2(A, U, G, G, T)
rna2: RNA2 = RNA2(A, U, G, G, T)
  
scala> rna2 take 3
res5: RNA2 = RNA2(A, U, G)
  
scala> rna2 filter (U !=)
res6: RNA2 = RNA2(A, G, G, T)

Next: Dealing with map and friends


Dealing with map and friendsTopIntegrating new collectionsAdapting the result type of RNA methodsContents