ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 02.01.2026

Просмотров: 3447

Скачиваний: 0

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

Section 25.3

Chapter 25 · The Architecture of Scala Collections

617

The second way to create an RNA1 value is provided by the apply method in the RNA1 object. It takes a variable number of Base arguments and simply forwards them as a sequence to fromSeq. Here are the two creation schemes in action:

scala> val xs = List(A, G, T, A)

xs: List[Product with Base] = List(A, G, T, A)

scala> RNA1.fromSeq(xs)

res1: RNA1 = RNA1(A, G, T, A)

scala> val rna1 = RNA1(A, U, G, G, T) rna1: RNA1 = RNA1(A, U, G, G, T)

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 Listing 25.6 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.

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))

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 25.3

Chapter 25 · The Architecture of Scala Collections

618

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

}

Listing 25.7 · RNA strands class, second version.

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 Listing 25.7. 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., RNA2 in Listing 25.7.

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).

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 25.3

Chapter 25 · The Architecture of Scala Collections

619

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 Listing 25.7 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 RNA class in Listing 25.7, 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)

Dealing with map and friends

However, there is another class of methods in collections that are not dealt with yet. These methods do not always return the collection type exactly.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 25.3

Chapter 25 · The Architecture of Scala Collections

620

They might return the same kind of collection, but with a different element type. The classical example of this is the map method. If s is a Seq[Int], and f is a function from Int to String, then s.map(f) would return a Seq[String]. So the element type changes between the receiver and the result, but the kind of collection stays the same.

There are a number of other methods that behave like map. For some of them you would expect this (e.g., flatMap, collect), but for others you might not. For instance, the append method, ++, also might return a result of different type as its arguments—appending a list of String to a list of Int would give a list of Any. How should these methods be adapted to RNA strands? Ideally we’d expect that mapping bases to bases over an RNA strand would yield again an RNA strand:

scala> val rna = RNA(A, U, G, G, T) rna: RNA = RNA(A, U, G, G, T)

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

Likewise, appending two RNA strands with ++ should yield again another RNA strand:

scala> rna ++ rna

res8: RNA = RNA(A, U, G, G, T, A, U, G, G, T)

On the other hand, mapping bases to some other type over an RNA strand cannot yield another RNA strand because the new elements have the wrong type. It has to yield a sequence instead. In the same vein appending elements that are not of type Base to an RNA strand can yield a general sequence, but it cannot yield another RNA strand.

scala> rna map Base.toInt

res2: IndexedSeq[Int] = Vector(0, 3, 2, 2, 1)

scala> rna ++ List("missing", "data") res3: IndexedSeq[java.lang.Object] =

Vector(A, U, G, G, T, missing, data)

This is what you’d expect in the ideal case. But this is not what the RNA2 class as given in Listing 25.7 provides. In fact, if you ran the first two examples above with instances of this class you would obtain:

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 25.3

Chapter 25 · The Architecture of Scala Collections

621

scala> val rna2 = RNA2(A, U, G, G, T) rna2: RNA2 = RNA2(A, U, G, G, T)

scala> rna2 map { case A => T case b => b } res0: IndexedSeq[Base] = Vector(T, U, G, G, T)

scala> rna2 ++ rna2

res1: IndexedSeq[Base] = Vector(A, U, G, G, T, A, U, G, G, T)

So the result of map and ++ is never an RNA strand, even if the element type of the generated collection is a Base. To see how to do better, it pays to have a close look at the signature of the map method (or of ++, which has a similar signature). The map method is originally defined in class scala.collection.TraversableLike with the following signature:

def map[B, That](f: A => B)

(implicit cbf: CanBuildFrom[Repr, B, That]): That

Here A is the type of elements of the collection, and Repr is the type of the collection itself, that is, the second type parameter that gets passed to implementation classes such as TraversableLike and IndexedSeqLike. The map method takes two more type parameters, B and That. The B parameter stands for the result type of the mapping function, which is also the element type of the new collection. The That appears as the result type of map, so it represents the type of the new collection that gets created.

How is the That type determined? In fact it is linked to the other types by an implicit parameter cbf, of type CanBuildFrom[Repr, B, That]. These CanBuildFrom implicits are defined by the individual collection classes. In essence, an implicit value of type CanBuildFrom[From, Elem, To] says: “Here is a way, given a collection of type From, to build with elements of type Elem a collection of type To.”

Now the behavior of map and ++ on RNA2 sequences becomes clearer. There is no CanBuildFrom instance that creates RNA2 sequences, so the next best available CanBuildFrom was found in the companion object of the inherited trait IndexedSeq. That implicit creates IndexedSeqs, and that’s what you saw when applying map to rna2.

To address this shortcoming, you need to define an implicit instance of CanBuildFrom in the companion object of the RNA class. That instance should have type CanBuildFrom[RNA, Base, RNA]. Hence, this instance states that, given an RNA strand and a new element type Base, you can

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 25.3

Chapter 25 · The Architecture of Scala Collections

622

final class RNA private (val groups: Array[Int], val length: Int) extends IndexedSeq[Base] with IndexedSeqLike[Base, RNA] {

import RNA._

//Mandatory re-implementation of ‘newBuilder‘ in ‘IndexedSeq‘ override protected[this] def newBuilder: Builder[Base, RNA] =

RNA.newBuilder

//Mandatory implementation of ‘apply‘ in ‘IndexedSeq‘

def apply(idx: Int): Base = { if (idx < 0 || length <= idx)

throw new IndexOutOfBoundsException Base.fromInt(groups(idx / N) >> (idx % N * S) & M)

}

//Optional re-implementation of foreach,

//to make it more efficient.

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

}

}

}

Listing 25.8 · RNA strands class, final version.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


N * S)

Section 25.3

Chapter 25 · The Architecture of Scala Collections

623

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 % 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

}

}

Listing 25.9 · RNA companion object—final version.

build another collection which is again an RNA strand. Listing 25.8 and Listing 25.9 show the details. Compared to class RNA2 there are two important differences. First, the newBuilder implementation has moved from the RNA class to its companion object. The newBuilder method in class RNA simply forwards to this definition. Second, there is now an implicit CanBuildFrom value in object RNA. To create such an object you need to define two apply methods in the CanBuildFrom trait. Both create a new builder for an RNA collection, but they differ in their argument list. The apply() method simply creates a new builder of the right type. By contrast, the apply(from) method takes the original collection as argument. This can be useful to adapt the dynamic type of builder’s return type to be the same as the dynamic type of the receiver. In the case of RNA this does not come into play because RNA is a final class, so any receiver of static type RNA

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index