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

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

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

Добавлен: 02.01.2026

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

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

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

Section 24.20

Chapter 24 · The Scala Collections API

606

changed semantics: As of 2.8, keys returns Iterable[A]

rather than Iterator[A].

m.keys

ˆ

res2: Iterable[Int] = Set(1, 3)

Two parts of the old libraries were replaced wholesale. For these deprecation warnings were not feasible.

1.The previous scala.collection.jcl package is gone. This package tried to mimic some of the Java collection library design in Scala, but in doing so broke many symmetries. Most people who wanted Java collections bypassed jcl and used java.util directly. Scala 2.8 offers automatic conversion mechanisms between both collection libraries in the JavaConversions object, described in Section 24.18, which replaces the jcl package.

2.Projections have been generalized and cleaned up and are now available as views. It seems that projections were used rarely, so not much code should be affected by this change.

So, if your code uses either jcl or projections there might be some minor rewriting to do.

24.20Conclusion

You’ve now seen how to use Scala’s collection in great detail. Scala’s collections take the approach of giving you powerful building blocks rather than a number of ad hoc utility methods. Putting together two or three such building blocks allows you to express an enormous number of useful computations. This style of library is especially effective due to Scala having a light syntax for function literals, and due to it providing many collection types that are persistent and immutable.

This chapter has shown collections from the point of view of a programmer using the collection library. The next chapter will show you how the collections are built and how you can add your own collection types.

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


Chapter 25

The Architecture of Scala Collections

This chapter describes the architecture of the Scala collections framework in detail. Compared to what you learned in Chapter 24 you will find out more about the internal workings of the framework. You will also learn how this architecture helps you define your own collections in a few lines of code, while reusing the overwhelming part of collection functionality from the framework.

Chapter 24 enumerated a large number of collection operations, which exist uniformly on many different collection implementations. Implementing every collection operation anew for every collection type would lead to an enormous amount of code, most of which would be copied from somewhere else. Such code duplication could lead to inconsistencies over time, when an operation is added or modified in one part of the collection library but not in others. The principal design objective of the new collections framework was to avoid any duplication, defining every operation in as few places as possible.1 The design approach was to implement most operations in collection “templates” that can be flexibly inherited from individual base classes and implementations. This chapter explains these templates and other classes and traits that constitute the “building blocks” of the framework, as well as the construction principles they support.

1Ideally, everything should be defined in one place only, but there are a few exceptions where things needed to be redefined.

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

Section 25.1

Chapter 25 · The Architecture of Scala Collections

608

package scala.collection.generic

class Builder[-Elem, +To] { def +=(elem: Elem): this.type def result(): To

def clear()

def mapResult(f: To => NewTo): Builder[Elem, NewTo] = ...

}

Listing 25.1 · An outline of the Builder class.

25.1 Builders

Almost all collection operations are implemented in terms of traversals and builders. Traversals are handled by Traversable’s foreach method, and building new collections is handled by instances of class Builder. Listing 25.1 presents a slightly abbreviated outline of this class.

You can add an element x to a builder b with b += x. There’s also syntax to add more than one element at once, for instance b += (x, y), and b ++= xs work as for buffers (in fact, buffers are an enriched version of builders). The result() method returns a collection from a builder. The state of the builder is undefined after taking its result, but it can be reset into a new empty state using clear(). Builders are generic in both the element type, Elem, and in the type, To, of collections they return.

Often, a builder can refer to some other builder for assembling the elements of a collection, but then would like to transform the result of the other builder, for example to give it a different type. This task is simplified by method mapResult in class Builder. Suppose for instance you have an array buffer buf. Array buffers are builders for themselves, so taking the result() of an array buffer will return the same buffer. If you want to use this buffer to produce a builder that builds arrays, you can use mapResult like this:

scala> val buf = new ArrayBuffer[Int]

buf: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer()

scala> val bldr = buf mapResult (_.toArray)

bldr: scala.collection.mutable.Builder[Int,Array[Int]] = ArrayBuffer()

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


Section 25.2

Chapter 25 · The Architecture of Scala Collections

609

package scala.collection

 

 

class

TraversableLike[+Elem, +Repr] {

 

 

def

newBuilder: Builder[Elem, Repr] //

deferred

def

foreach[U](f: Elem => U)

//

deferred

 

...

 

 

def

filter(p: Elem => Boolean): Repr =

{

val b = newBuilder

 

 

foreach { elem => if (p(elem)) b += elem } b.result

}

}

Listing 25.2 · Implementation of filter in TraversableLike.

The result value, bldr, is a builder that uses the array buffer, buf, to collect elements. When a result is demanded from bldr, the result of buf is computed, which yields the array buffer buf itself. This array buffer is then mapped with _.toArray to an array. So the end result is that bldr is a builder for arrays.

25.2 Factoring out common operations

The main design objectives of the collection library redesign were to have, at the same time, natural types and maximal sharing of implementation code. In particular, Scala’s collections follow the “same-result-type” principle: wherever possible, a transformation method on a collection will yield a collection of the same type. For instance, the filter operation should yield, on every collection type, an instance of the same collection type. Applying filter on a List should give a List; applying it on a Map should give a Map, and so on. In the rest of this section, you will find out how this is achieved.

The fast track

The material in this section is a bit more dense than usual and might require some time to absorb. If you want to move ahead quickly, you could skip the remainder of this section and move on to Section 25.3 on

page 614 where you will learn with concrete examples how to integrate your own collection classes in the framework.

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


Section 25.2

Chapter 25 · The Architecture of Scala Collections

610

The Scala collection library avoids code duplication and achieves the “same-result-type” principle by using generic builders and traversals over collections in so-called implementation traits. These traits are named with a Like suffix; for instance, IndexedSeqLike is the implementation trait for IndexedSeq, and similarly, TraversableLike is the implementation trait for Traversable. Collection classes such as Traversable or IndexedSeq inherit all their concrete method implementations from these traits. Implementation traits have two type parameters instead of one for normal collections. They parameterize not only over the collection’s element type, but also over the collection’s representation type, i.e., the type of the underlying collection, such as Seq[I] or List[T]. For instance, here is the header of trait TraversableLike:

trait TraversableLike[+Elem, +Repr] { ... }

The type parameter, Elem, stands for the element type of the traversable whereas the type parameter Repr stands for its representation. There are no constraints on Repr. In particular Repr might be instantiated to a type that is itself not a subtype of Traversable. That way, classes outside the collections hierarchy such as String and Array can still make use of all operations defined in a collection implementation trait.

Taking filter as an example, this operation is defined once for all collection classes in the trait TraversableLike. An outline of the relevant code is shown in Listing 25.2. The trait declares two abstract methods, newBuilder and foreach, which are implemented in concrete collection classes. The filter operation is implemented in the same way for all collections using these methods. It first constructs a new builder for the representation type Repr, using newBuilder. It then traverses all elements of the current collection, using foreach. If an element x satisfies the given predicate p (i.e., p(x) is true), it is added with the builder. Finally, the elements collected in the builder are returned as an instance of the Repr collection type by calling the builder’s result method.

A bit more complicated is the map operation on collections. For instance, if f is a function from String to Int, and xs is a List[String], then xs map f should give a List[Int]. Likewise, if ys is an Array[String], then ys map f should give a Array[Int]. The problem is how to achieve that without duplicating the definition of the map method in lists and arrays. The newBuilder/foreach framework shown in Listing 25.2 is not sufficient for this because it only allows creation of new instances of the same collection

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


Section 25.2

Chapter 25 · The Architecture of Scala Collections

611

type whereas map needs an instance of the same collection type constructor, but possibly with a different element type.

What’s more, even the result type constructor of a function like map might depend in non-trivial ways on the other argument types. Here is an example:

scala> import collection.immutable.BitSet import collection.immutable.BitSet

scala> val bits = BitSet(1, 2, 3)

bits: scala.collection.immutable.BitSet = BitSet(1, 2, 3)

scala> bits map (_ * 2)

res13: scala.collection.immutable.BitSet = BitSet(2, 4, 6)

scala> bits map (_.toFloat)

res14: scala.collection.immutable.Set[Float] = Set(1.0, 2.0, 3.0)

If you map the doubling function _ * 2 over a bit set you obtain another bit set. However, if you map the function (_.toFloat) over the same bit set, the result is a general Set[Float]. Of course, it can’t be a bit set because bit sets contain Ints, not Floats.

Note that map’s result type depends on the type of function that’s passed to it. If the result type of that function argument is again an Int, the result of map is a BitSet, but if the result type of the function argument is something else, the result of map is just a Set. You’ll find out soon how this typeflexibility is achieved in Scala.

The problem with BitSet is not an isolated case. Here are two more interactions with the interpreter that both map a function over a map:

scala> Map("a" -> 1, "b" -> 2) map { case (x, y) => (y, x) } res3: scala.collection.immutable.Map[Int,java.lang.String]

= Map(1 -> a, 2 -> b)

scala> Map("a" -> 1, "b" -> 2) map { case (x, y) => y } res4: scala.collection.immutable.Iterable[Int]

= List(1, 2)

The first function swaps two arguments of a key/value pair. The result of mapping this function is again a map, but now going in the other direction. In fact, the first expression yields the inverse of the original map, provided

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