Skip to content

Commit 7f0a471

Browse files
authoredOct 7, 2022
Merge pull request #2585 from flomebul/custom-collections
Add code tabs for overviews/custom-collections
2 parents 63f25ac + 03ad78f commit 7f0a471

File tree

1 file changed

+708
-150
lines changed

1 file changed

+708
-150
lines changed
 

‎_overviews/core/custom-collections.md

Lines changed: 708 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,21 @@ to choose `Seq` because our collection can contain duplicates and
2727
iteration order is determined by insertion order. However, some
2828
[properties of `Seq`](/overviews/collections/seqs.html) are not satisfied:
2929

30+
{% tabs notCapped_1 %}
31+
{% tab 'Scala 2 and 3' for=notCapped_1 %}
3032
~~~ scala
3133
(xs ++ ys).size == xs.size + ys.size
3234
~~~
35+
{% endtab %}
36+
{% endtabs %}
3337

3438
Consequently, the only sensible choice as a base collection type
3539
is `collection.immutable.Iterable`.
3640

3741
### First version of `Capped` class ###
3842

43+
{% tabs capped1_1 class=tabs-scala-version %}
44+
{% tab 'Scala 2' for=capped1_1 %}
3945
~~~ scala
4046
import scala.collection._
4147

@@ -72,11 +78,54 @@ class Capped1[A] private (val capacity: Int, val length: Int, offset: Int, elems
7278
elem
7379
}
7480
}
75-
81+
7682
override def className = "Capped1"
7783

7884
}
7985
~~~
86+
{% endtab %}
87+
{% tab 'Scala 3' for=capped1_1 %}
88+
~~~scala
89+
import scala.collection.*
90+
91+
class Capped1[A] private (val capacity: Int, val length: Int, offset: Int, elems: Array[Any])
92+
extends immutable.Iterable[A]:
93+
self =>
94+
95+
def this(capacity: Int) =
96+
this(capacity, length = 0, offset = 0, elems = Array.ofDim(capacity))
97+
98+
def appended[B >: A](elem: B): Capped1[B] =
99+
val newElems = Array.ofDim[Any](capacity)
100+
Array.copy(elems, 0, newElems, 0, capacity)
101+
val (newOffset, newLength) =
102+
if length == capacity then
103+
newElems(offset) = elem
104+
((offset + 1) % capacity, length)
105+
else
106+
newElems(length) = elem
107+
(offset, length + 1)
108+
Capped1[B](capacity, newLength, newOffset, newElems)
109+
end appended
110+
111+
inline def :+ [B >: A](elem: B): Capped1[B] = appended(elem)
112+
113+
def apply(i: Int): A = elems((i + offset) % capacity).asInstanceOf[A]
114+
115+
def iterator: Iterator[A] = new AbstractIterator[A]:
116+
private var current = 0
117+
def hasNext = current < self.length
118+
def next(): A =
119+
val elem = self(current)
120+
current += 1
121+
elem
122+
end iterator
123+
124+
override def className = "Capped1"
125+
end Capped1
126+
~~~
127+
{% endtab %}
128+
{% endtabs %}
80129

81130
The above listing presents the first version of our capped collection
82131
implementation. It will be refined later. The class `Capped1` has a
@@ -100,33 +149,37 @@ the `offset`.
100149
These two methods, `appended` and `apply`, implement the specific
101150
behavior of the `Capped1` collection type. In addition to them, we have
102151
to implement `iterator` to make the generic collection operations
103-
(such as `foldLeft`, `count`, etc.) work on `Capped` collections.
152+
(such as `foldLeft`, `count`, etc.) work on `Capped1` collections.
104153
Here we implement it by using indexed access.
105154

106155
Last, we override `className` to return the name of the collection,
107-
“Capped1”. This name is used by the `toString` operation.
156+
`“Capped1”`. This name is used by the `toString` operation.
108157

109158
Here are some interactions with the `Capped1` collection:
110159

160+
{% tabs capped1_2 %}
161+
{% tab 'Scala 2 and 3' for=capped1_2 %}
111162
~~~ scala
112-
scala> new Capped1(capacity = 4)
113-
res0: Capped1[Nothing] = Capped1()
163+
scala> val c0 = new Capped1(capacity = 4)
164+
val c0: Capped1[Nothing] = Capped1()
114165

115-
scala> res0 :+ 1 :+ 2 :+ 3
116-
res1: Capped1[Int] = Capped1(1, 2, 3)
166+
scala> val c1 = c0 :+ 1 :+ 2 :+ 3
167+
val c1: Capped1[Int] = Capped1(1, 2, 3)
117168

118-
scala> res1.length
119-
res2: Int = 3
169+
scala> c1.length
170+
val res2: Int = 3
120171

121-
scala> res1.lastOption
122-
res3: Option[Int] = Some(3)
172+
scala> c1.lastOption
173+
val res3: Option[Int] = Some(3)
123174

124-
scala> res1 :+ 4 :+ 5 :+ 6
125-
res4: Capped1[Int] = Capped1(3, 4, 5, 6)
175+
scala> val c2 = c1 :+ 4 :+ 5 :+ 6
176+
val c2: Capped1[Int] = Capped1(3, 4, 5, 6)
126177

127-
scala> res4.take(3)
128-
res5: collection.immutable.Iterable[Int] = List(3, 4, 5)
178+
scala> val c3 = c2.take(3)
179+
val c3: collection.immutable.Iterable[Int] = List(3, 4, 5)
129180
~~~
181+
{% endtab %}
182+
{% endtabs %}
130183

131184
You can see that if we try to grow the collection with more than four
132185
elements, the first elements are dropped (see `res4`). The operations
@@ -144,7 +197,13 @@ question should be what needs to be done to change them? One way to do
144197
this would be to override the `take` method in class `Capped1`, maybe like
145198
this:
146199

147-
def take(count: Int): Capped1 = …
200+
{% tabs take_signature %}
201+
{% tab 'Scala 2 and 3' for=take_signature %}
202+
```scala
203+
def take(count: Int): Capped1 =
204+
```
205+
{% endtab %}
206+
{% endtabs %}
148207

149208
This would do the job for `take`. But what about `drop`, or `filter`, or
150209
`init`? In fact there are over fifty methods on collections that return
@@ -155,6 +214,8 @@ effect, as shown in the next section.
155214

156215
### Second version of `Capped` class ###
157216

217+
{% tabs capped2_1 class=tabs-scala-version %}
218+
{% tab 'Scala 2' for=capped2_1 %}
158219
~~~ scala
159220
import scala.collection._
160221

@@ -191,6 +252,44 @@ class Capped2Factory(capacity: Int) extends IterableFactory[Capped2] {
191252
}
192253
}
193254
~~~
255+
{% endtab %}
256+
{% tab 'Scala 3' for=capped2_1 %}
257+
~~~ scala
258+
class Capped2[A] private(val capacity: Int, val length: Int, offset: Int, elems: Array[Any])
259+
extends immutable.Iterable[A],
260+
IterableOps[A, Capped2, Capped2[A]]:
261+
self =>
262+
263+
def this(capacity: Int) = // as before
264+
265+
def appended[B >: A](elem: B): Capped2[B] = // as before
266+
inline def :+[B >: A](elem: B): Capped2[B] = // as before
267+
def apply(i: Int): A = // as before
268+
269+
def iterator: Iterator[A] = // as before
270+
271+
override def className = "Capped2"
272+
override val iterableFactory: IterableFactory[Capped2] = Capped2Factory(capacity)
273+
override protected def fromSpecific(coll: IterableOnce[A]): Capped2[A] = iterableFactory.from(coll)
274+
override protected def newSpecificBuilder: mutable.Builder[A, Capped2[A]] = iterableFactory.newBuilder
275+
override def empty: Capped2[A] = iterableFactory.empty
276+
end Capped2
277+
278+
class Capped2Factory(capacity: Int) extends IterableFactory[Capped2]:
279+
280+
def from[A](source: IterableOnce[A]): Capped2[A] =
281+
(newBuilder[A] ++= source).result()
282+
283+
def empty[A]: Capped2[A] = Capped2[A](capacity)
284+
285+
def newBuilder[A]: mutable.Builder[A, Capped2[A]] =
286+
new mutable.ImmutableBuilder[A, Capped2[A]](empty):
287+
def addOne(elem: A): this.type =
288+
elems = elems :+ elem; this
289+
end Capped2Factory
290+
~~~
291+
{% endtab %}
292+
{% endtabs %}
194293

195294
The Capped class needs to inherit not only from `Iterable`, but also
196295
from its implementation trait `IterableOps`. This is shown in the
@@ -229,31 +328,35 @@ With the refined implementation of the [`Capped2` class](#second-version-of-capp
229328
the transformation operations work now as expected, and the
230329
`Capped2Factory` class provides seamless conversions from other collections:
231330

331+
{% tabs capped2_2 %}
332+
{% tab 'Scala 2 and 3' for=capped2_2 %}
232333
~~~ scala
233334
scala> object Capped extends Capped2Factory(capacity = 4)
234335
defined object Capped
235336

236337
scala> Capped(1, 2, 3)
237-
res0: Capped2[Int] = Capped2(1, 2, 3)
338+
val res0: Capped2[Int] = Capped2(1, 2, 3)
238339

239340
scala> res0.take(2)
240-
res1: Capped2[Int] = Capped2(1, 2)
341+
val res1: Capped2[Int] = Capped2(1, 2)
241342

242343
scala> res0.filter(x => x % 2 == 1)
243-
res2: Capped2[Int] = Capped2(1, 3)
344+
val res2: Capped2[Int] = Capped2(1, 3)
244345

245346
scala> res0.map(x => x * x)
246-
res3: Capped2[Int] = Capped2(1, 4, 9)
347+
val res3: Capped2[Int] = Capped2(1, 4, 9)
247348

248349
scala> List(1, 2, 3, 4, 5).to(Capped)
249-
res4: Capped2[Int] = Capped2(2, 3, 4, 5)
350+
val res4: Capped2[Int] = Capped2(2, 3, 4, 5)
250351
~~~
352+
{% endtab %}
353+
{% endtabs %}
251354

252355
This implementation now behaves correctly, but we can still improve
253356
a few things:
254357

255358
- since our collection is strict, we can take advantage
256-
of the better performance offered by
359+
of the better performance offered by
257360
strict implementations of transformation operations,
258361
- since our `fromSpecific`, `newSpecificBuilder` and `empty`
259362
operation just forward to the `iterableFactory` member,
@@ -262,6 +365,8 @@ a few things:
262365

263366
### Final version of `Capped` class ###
264367

368+
{% tabs capped_1 class=tabs-scala-version %}
369+
{% tab 'Scala 2' for=capped_1 %}
265370
~~~ scala
266371
import scala.collection._
267372

@@ -324,6 +429,69 @@ class CappedFactory(capacity: Int) extends IterableFactory[Capped] {
324429

325430
}
326431
~~~
432+
{% endtab %}
433+
{% tab 'Scala 3' for=capped_1 %}
434+
~~~ scala
435+
import scala.collection.*
436+
437+
final class Capped[A] private (val capacity: Int, val length: Int, offset: Int, elems: Array[Any])
438+
extends immutable.Iterable[A],
439+
IterableOps[A, Capped, Capped[A]],
440+
IterableFactoryDefaults[A, Capped],
441+
StrictOptimizedIterableOps[A, Capped, Capped[A]]:
442+
self =>
443+
444+
def this(capacity: Int) =
445+
this(capacity, length = 0, offset = 0, elems = Array.ofDim(capacity))
446+
447+
def appended[B >: A](elem: B): Capped[B] =
448+
val newElems = Array.ofDim[Any](capacity)
449+
Array.copy(elems, 0, newElems, 0, capacity)
450+
val (newOffset, newLength) =
451+
if length == capacity then
452+
newElems(offset) = elem
453+
((offset + 1) % capacity, length)
454+
else
455+
newElems(length) = elem
456+
(offset, length + 1)
457+
Capped[B](capacity, newLength, newOffset, newElems)
458+
end appended
459+
460+
inline def :+ [B >: A](elem: B): Capped[B] = appended(elem)
461+
462+
def apply(i: Int): A = elems((i + offset) % capacity).asInstanceOf[A]
463+
464+
def iterator: Iterator[A] = view.iterator
465+
466+
override def view: IndexedSeqView[A] = new IndexedSeqView[A]:
467+
def length: Int = self.length
468+
def apply(i: Int): A = self(i)
469+
470+
override def knownSize: Int = length
471+
472+
override def className = "Capped"
473+
474+
override val iterableFactory: IterableFactory[Capped] = new CappedFactory(capacity)
475+
476+
end Capped
477+
478+
class CappedFactory(capacity: Int) extends IterableFactory[Capped]:
479+
480+
def from[A](source: IterableOnce[A]): Capped[A] =
481+
source match
482+
case capped: Capped[?] if capped.capacity == capacity => capped.asInstanceOf[Capped[A]]
483+
case _ => (newBuilder[A] ++= source).result()
484+
485+
def empty[A]: Capped[A] = Capped[A](capacity)
486+
487+
def newBuilder[A]: mutable.Builder[A, Capped[A]] =
488+
new mutable.ImmutableBuilder[A, Capped[A]](empty):
489+
def addOne(elem: A): this.type = { elems = elems :+ elem; this }
490+
491+
end CappedFactory
492+
~~~
493+
{% endtab %}
494+
{% endtabs %}
327495

328496
That is it. The final [`Capped` class](#final-version-of-capped-class):
329497

@@ -345,33 +513,58 @@ methods (such as `iterator` in our case), if any.
345513

346514
## RNA sequences ##
347515

348-
To start with the second example, we define the four RNA Bases:
349-
350-
abstract class Base
351-
case object A extends Base
352-
case object U extends Base
353-
case object G extends Base
354-
case object C extends Base
516+
To start with the second example, say you want to create a new immutable sequence type for RNA strands.
517+
These are sequences of bases A (adenine), U (uracil), G (guanine), and C
518+
(cytosine). The definitions for bases are set up as shown in the
519+
listing of RNA bases below:
355520

356-
object Base {
357-
val fromInt: Int => Base = Array(A, U, G, C)
358-
val toInt: Base => Int = Map(A -> 0, U -> 1, G -> 2, C -> 3)
359-
}
360-
361-
Say you want to create a new immutable sequence type for RNA strands, which are
362-
sequences of bases A (adenine), U (uracil), G (guanine), and C
363-
(cytosine). The definitions for bases are easily set up as shown in the
364-
listing of RNA bases above.
521+
{% tabs Base_1 class=tabs-scala-version %}
522+
{% tab 'Scala 2' for=Base_1 %}
523+
~~~ scala
524+
abstract class Base
525+
case object A extends Base
526+
case object U extends Base
527+
case object G extends Base
528+
case object C extends Base
529+
530+
object Base {
531+
val fromInt: Int => Base = Array(A, U, G, C)
532+
val toInt: Base => Int = Map(A -> 0, U -> 1, G -> 2, C -> 3)
533+
}
534+
~~~
365535

366536
Every base is defined as a case object that inherits from a common
367537
abstract class `Base`. The `Base` class has a companion object that
368538
defines two functions that map between bases and the integers 0 to 3.
369-
You can see in the examples two different ways to use collections
539+
540+
You can see in the above example two different ways to use collections
370541
to implement these functions. The `toInt` function is implemented as a
371542
`Map` from `Base` values to integers. The reverse function, `fromInt`, is
372543
implemented as an array. This makes use of the fact that both maps and
373544
arrays *are* functions because they inherit from the `Function1` trait.
374545

546+
{% endtab %}
547+
{% tab 'Scala 3' for=Base_1 %}
548+
~~~ scala
549+
enum Base:
550+
case A, U, G, C
551+
552+
object Base:
553+
val fromInt: Int => Base = values
554+
val toInt: Base => Int = _.ordinal
555+
~~~
556+
557+
Every base is defined as a case of the `Base` enum. `Base` has a companion object
558+
that defines two functions that map between bases and the integers 0 to 3.
559+
560+
The `toInt` function is implemented by delegating to the `ordinal` method defined on `Base`,
561+
which is automatically defined because `Base` is an enum. Each enum case will have a unique `ordinal` value.
562+
The reverse function, `fromInt`, is implemented as an array. This makes use of the fact that
563+
arrays *are* functions because they inherit from the `Function1` trait.
564+
565+
{% endtab %}
566+
{% endtabs %}
567+
375568
The next task is to define a class for strands of RNA. Conceptually, a
376569
strand of RNA is simply a `Seq[Base]`. However, RNA strands can get
377570
quite long, so it makes sense to invest some work in a compact
@@ -383,51 +576,104 @@ representation.
383576

384577
### First version of RNA strands class ###
385578

386-
import collection.mutable
387-
import collection.immutable.{ IndexedSeq, IndexedSeqOps }
579+
{% tabs RNA1_1 class=tabs-scala-version %}
580+
{% tab 'Scala 2' for=RNA1_1 %}
581+
~~~ scala
582+
import collection.mutable
583+
import collection.immutable.{ IndexedSeq, IndexedSeqOps }
388584

389-
final class RNA1 private (
390-
val groups: Array[Int],
391-
val length: Int
392-
) extends IndexedSeq[Base]
393-
with IndexedSeqOps[Base, IndexedSeq, RNA1] {
585+
final class RNA1 private (
586+
val groups: Array[Int],
587+
val length: Int
588+
) extends IndexedSeq[Base]
589+
with IndexedSeqOps[Base, IndexedSeq, RNA1] {
394590

395-
import RNA1._
591+
import RNA1._
396592

397-
def apply(idx: Int): Base = {
398-
if (idx < 0 || length <= idx)
399-
throw new IndexOutOfBoundsException
400-
Base.fromInt(groups(idx / N) >> (idx % N * S) & M)
401-
}
593+
def apply(idx: Int): Base = {
594+
if (idx < 0 || length <= idx)
595+
throw new IndexOutOfBoundsException
596+
Base.fromInt(groups(idx / N) >> (idx % N * S) & M)
597+
}
402598

403-
override protected def fromSpecific(coll: IterableOnce[Base]): RNA1 =
404-
fromSeq(coll.iterator.toSeq)
405-
override protected def newSpecificBuilder: mutable.Builder[Base, RNA1] =
406-
iterableFactory.newBuilder[Base].mapResult(fromSeq)
407-
override def empty: RNA1 = fromSeq(Seq.empty)
408-
override def className = "RNA1"
409-
}
599+
override protected def fromSpecific(coll: IterableOnce[Base]): RNA1 =
600+
fromSeq(coll.iterator.toSeq)
601+
override protected def newSpecificBuilder: mutable.Builder[Base, RNA1] =
602+
iterableFactory.newBuilder[Base].mapResult(fromSeq)
603+
override def empty: RNA1 = fromSeq(Seq.empty)
604+
override def className = "RNA1"
605+
}
410606

411-
object RNA1 {
607+
object RNA1 {
412608

413-
// Number of bits necessary to represent group
414-
private val S = 2
609+
// Number of bits necessary to represent group
610+
private val S = 2
415611

416-
// Number of groups that fit in an Int
417-
private val N = 32 / S
612+
// Number of groups that fit in an Int
613+
private val N = 32 / S
418614

419-
// Bitmask to isolate a group
420-
private val M = (1 << S) - 1
615+
// Bitmask to isolate a group
616+
private val M = (1 << S) - 1
421617

422-
def fromSeq(buf: collection.Seq[Base]): RNA1 = {
423-
val groups = new Array[Int]((buf.length + N - 1) / N)
424-
for (i <- 0 until buf.length)
425-
groups(i / N) |= Base.toInt(buf(i)) << (i % N * S)
426-
new RNA1(groups, buf.length)
427-
}
618+
def fromSeq(buf: collection.Seq[Base]): RNA1 = {
619+
val groups = new Array[Int]((buf.length + N - 1) / N)
620+
for (i <- 0 until buf.length)
621+
groups(i / N) |= Base.toInt(buf(i)) << (i % N * S)
622+
new RNA1(groups, buf.length)
623+
}
428624

429-
def apply(bases: Base*) = fromSeq(bases)
430-
}
625+
def apply(bases: Base*) = fromSeq(bases)
626+
}
627+
~~~
628+
{% endtab %}
629+
{% tab 'Scala 3' for=RNA1_1 %}
630+
~~~ scala
631+
import collection.mutable
632+
import collection.immutable.{ IndexedSeq, IndexedSeqOps }
633+
634+
final class RNA1 private
635+
( val groups: Array[Int],
636+
val length: Int
637+
) extends IndexedSeq[Base],
638+
IndexedSeqOps[Base, IndexedSeq, RNA1]:
639+
640+
import RNA1.*
641+
642+
def apply(idx: Int): Base =
643+
if idx < 0 || length <= idx then
644+
throw IndexOutOfBoundsException()
645+
Base.fromInt(groups(idx / N) >> (idx % N * S) & M)
646+
647+
override protected def fromSpecific(coll: IterableOnce[Base]): RNA1 =
648+
fromSeq(coll.iterator.toSeq)
649+
override protected def newSpecificBuilder: mutable.Builder[Base, RNA1] =
650+
iterableFactory.newBuilder[Base].mapResult(fromSeq)
651+
override def empty: RNA1 = fromSeq(Seq.empty)
652+
override def className = "RNA1"
653+
end RNA1
654+
655+
object RNA1:
656+
657+
// Number of bits necessary to represent group
658+
private val S = 2
659+
660+
// Number of groups that fit in an Int
661+
private val N = 32 / S
662+
663+
// Bitmask to isolate a group
664+
private val M = (1 << S) - 1
665+
666+
def fromSeq(buf: collection.Seq[Base]): RNA1 =
667+
val groups = new Array[Int]((buf.length + N - 1) / N)
668+
for i <- 0 until buf.length do
669+
groups(i / N) |= Base.toInt(buf(i)) << (i % N * S)
670+
new RNA1(groups, buf.length)
671+
672+
def apply(bases: Base*) = fromSeq(bases)
673+
end RNA1
674+
~~~
675+
{% endtab %}
676+
{% endtabs %}
431677

432678
The [RNA strands class listing](#first-version-of-rna-strands-class) above
433679
presents the first version of this
@@ -484,14 +730,22 @@ in the `RNA1` object. It takes a variable number of `Base` arguments and
484730
simply forwards them as a sequence to `fromSeq`. Here are the two
485731
creation schemes in action:
486732

487-
scala> val xs = List(A, G, U, A)
488-
xs: List[Base] = List(A, G, U, A)
733+
{% tabs RNA1_2 %}
734+
{% tab 'Scala 2 and 3' for=RNA1_2 %}
735+
736+
```scala
737+
scala> val xs = List(A, G, U, A)
738+
val xs: List[Base] = List(A, G, U, A)
739+
740+
scala> RNA1.fromSeq(xs)
741+
val res1: RNA1 = RNA1(A, G, U, A)
489742

490-
scala> RNA1.fromSeq(xs)
491-
res1: RNA1 = RNA1(A, G, U, A)
743+
scala> val rna1 = RNA1(A, U, G, G, C)
744+
val rna1: RNA1 = RNA1(A, U, G, G, C)
745+
```
492746

493-
scala> val rna1 = RNA1(A, U, G, G, C)
494-
rna1: RNA1 = RNA1(A, U, G, G, C)
747+
{% endtab %}
748+
{% endtabs %}
495749

496750
Also note that the type parameters of the `IndexedSeqOps` trait that
497751
we inherit from are: `Base`, `IndexedSeq` and `RNA1`. The first one
@@ -507,11 +761,19 @@ third one is `RNA1`. This means that operations like `map` or
507761

508762
Here is an example showing the usage of `take` and `filter`:
509763

510-
scala> rna1.take(3)
511-
res5: RNA1 = RNA1(A, U, G)
764+
{% tabs RNA1_3 %}
765+
{% tab 'Scala 2 and 3' for=RNA1_3 %}
766+
767+
```scala
768+
scala> val rna1_2 = rna1.take(3)
769+
val rna1_2: RNA1 = RNA1(A, U, G)
770+
771+
scala> val rna1_3 = rna1.filter(_ != U)
772+
val rna1_3: RNA1 = RNA1(A, G, G, C)
773+
```
512774

513-
scala> rna1.filter(_ != U)
514-
res6: RNA1 = RNA1(A, G, G, C)
775+
{% endtab %}
776+
{% endtabs %}
515777

516778
### Dealing with map and friends ###
517779

@@ -523,41 +785,65 @@ methods be adapted to RNA strands? The desired behavior would be to get
523785
back an RNA strand when mapping bases to bases or appending two RNA strands
524786
with `++`:
525787

526-
scala> val rna = RNA(A, U, G, G, C)
527-
rna: RNA = RNA(A, U, G, G, C)
788+
{% tabs RNA1_4 %}
789+
{% tab 'Scala 2 and 3' for=RNA1_4 %}
528790

529-
scala> rna map { case A => U case b => b }
530-
res7: RNA = RNA(U, U, G, G, C)
791+
```scala
792+
scala> val rna = RNA(A, U, G, G, C)
793+
val rna: RNA = RNA(A, U, G, G, C)
531794

532-
scala> rna ++ rna
533-
res8: RNA = RNA(A, U, G, G, C, A, U, G, G, C)
795+
scala> rna.map { case A => U case b => b }
796+
val res7: RNA = RNA(U, U, G, G, C)
797+
798+
scala> rna ++ rna
799+
val res8: RNA = RNA(A, U, G, G, C, A, U, G, G, C)
800+
```
801+
802+
{% endtab %}
803+
{% endtabs %}
534804

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

541-
scala> rna map Base.toInt
542-
res2: IndexedSeq[Int] = Vector(0, 1, 2, 2, 3)
811+
{% tabs RNA1_5 %}
812+
{% tab 'Scala 2 and 3' for=RNA1_5 %}
813+
814+
```scala
815+
scala> rna.map(Base.toInt)
816+
val res2: IndexedSeq[Int] = Vector(0, 1, 2, 2, 3)
817+
818+
scala> rna ++ List("missing", "data")
819+
val res3: IndexedSeq[java.lang.Object] =
820+
Vector(A, U, G, G, C, missing, data)
821+
```
543822

544-
scala> rna ++ List("missing", "data")
545-
res3: IndexedSeq[java.lang.Object] =
546-
Vector(A, U, G, G, C, missing, data)
823+
{% endtab %}
824+
{% endtabs %}
547825

548826
This is what you'd expect in the ideal case. But this is not what the
549827
[`RNA1` class](#first-version-of-rna-strands-class) provides. In fact, all
550828
examples will return instances of `Vector`, not just the last two. If you run
551829
the first three commands above with instances of this class you obtain:
552830

553-
scala> val rna1 = RNA1(A, U, G, G, C)
554-
rna1: RNA1 = RNA1(A, U, G, G, C)
831+
{% tabs RNA1_6 %}
832+
{% tab 'Scala 2 and 3' for=RNA1_6 %}
555833

556-
scala> rna1 map { case A => U case b => b }
557-
res0: IndexedSeq[Base] = Vector(U, U, G, G, C)
834+
```scala
835+
scala> val rna1 = RNA1(A, U, G, G, C)
836+
val rna1: RNA1 = RNA1(A, U, G, G, C)
558837

559-
scala> rna1 ++ rna1
560-
res1: IndexedSeq[Base] = Vector(A, U, G, G, C, A, U, G, G, C)
838+
scala> rna1.map { case A => U case b => b }
839+
val res0: IndexedSeq[Base] = Vector(U, U, G, G, C)
840+
841+
scala> rna1 ++ rna1
842+
val res1: IndexedSeq[Base] = Vector(A, U, G, G, C, A, U, G, G, C)
843+
```
844+
845+
{% endtab %}
846+
{% endtabs %}
561847

562848
So the result of `map` and `++` is never an RNA strand, even if the
563849
element type of the generated collection is `Base`. To see how to do
@@ -566,7 +852,13 @@ method (or of `++`, which has a similar signature). The `map` method is
566852
originally defined in class `scala.collection.IterableOps` with the
567853
following signature:
568854

569-
def map[B](f: A => B): CC[B]
855+
{% tabs map_signature %}
856+
{% tab 'Scala 2 and 3' for=map_signature %}
857+
```scala
858+
def map[B](f: A => B): CC[B]
859+
```
860+
{% endtab %}
861+
{% endtabs %}
570862

571863
Here `A` is the type of elements of the collection, and `CC` is the type
572864
constructor passed as a second parameter to the `IterableOps` trait.
@@ -576,38 +868,84 @@ this is why we always get a `Vector` as a result.
576868

577869
### Second version of RNA strands class ###
578870

579-
import scala.collection.{ View, mutable }
580-
import scala.collection.immutable.{ IndexedSeq, IndexedSeqOps }
581-
582-
final class RNA2 private (val groups: Array[Int], val length: Int)
583-
extends IndexedSeq[Base] with IndexedSeqOps[Base, IndexedSeq, RNA2] {
584-
585-
import RNA2._
586-
587-
def apply(idx: Int): Base = // as before
588-
override protected def fromSpecific(coll: IterableOnce[Base]): RNA2 = // as before
589-
override protected def newSpecificBuilder: mutable.Builder[Base, RNA2] = // as before
590-
591-
// Overloading of `appended`, `prepended`, `appendedAll`,
592-
// `prependedAll`, `map`, `flatMap` and `concat` to return an `RNA2`
593-
// when possible
594-
def concat(suffix: IterableOnce[Base]): RNA2 =
595-
fromSpecific(iterator ++ suffix.iterator)
596-
// symbolic alias for `concat`
597-
@inline final def ++ (suffix: IterableOnce[Base]): RNA2 = concat(suffix)
598-
def appended(base: Base): RNA2 =
599-
fromSpecific(new View.Appended(this, base))
600-
def appendedAll(suffix: IterableOnce[Base]): RNA2 =
601-
concat(suffix)
602-
def prepended(base: Base): RNA2 =
603-
fromSpecific(new View.Prepended(base, this))
604-
def prependedAll(prefix: IterableOnce[Base]): RNA2 =
605-
fromSpecific(prefix.iterator ++ iterator)
606-
def map(f: Base => Base): RNA2 =
607-
fromSpecific(new View.Map(this, f))
608-
def flatMap(f: Base => IterableOnce[Base]): RNA2 =
609-
fromSpecific(new View.FlatMap(this, f))
610-
}
871+
{% tabs RNA2_1 class=tabs-scala-version %}
872+
{% tab 'Scala 2' for=RNA2_1 %}
873+
~~~ scala
874+
import scala.collection.{ View, mutable }
875+
import scala.collection.immutable.{ IndexedSeq, IndexedSeqOps }
876+
877+
final class RNA2 private (val groups: Array[Int], val length: Int)
878+
extends IndexedSeq[Base] with IndexedSeqOps[Base, IndexedSeq, RNA2] {
879+
880+
import RNA2._
881+
882+
def apply(idx: Int): Base = // as before
883+
override protected def fromSpecific(coll: IterableOnce[Base]): RNA2 = // as before
884+
override protected def newSpecificBuilder: mutable.Builder[Base, RNA2] = // as before
885+
override def empty: RNA2 = // as before
886+
override def className = "RNA2"
887+
888+
// Overloading of `appended`, `prepended`, `appendedAll`,
889+
// `prependedAll`, `map`, `flatMap` and `concat` to return an `RNA2`
890+
// when possible
891+
def concat(suffix: IterableOnce[Base]): RNA2 =
892+
fromSpecific(iterator ++ suffix.iterator)
893+
// symbolic alias for `concat`
894+
@inline final def ++ (suffix: IterableOnce[Base]): RNA2 = concat(suffix)
895+
def appended(base: Base): RNA2 =
896+
fromSpecific(new View.Appended(this, base))
897+
def appendedAll(suffix: IterableOnce[Base]): RNA2 =
898+
concat(suffix)
899+
def prepended(base: Base): RNA2 =
900+
fromSpecific(new View.Prepended(base, this))
901+
def prependedAll(prefix: IterableOnce[Base]): RNA2 =
902+
fromSpecific(prefix.iterator ++ iterator)
903+
def map(f: Base => Base): RNA2 =
904+
fromSpecific(new View.Map(this, f))
905+
def flatMap(f: Base => IterableOnce[Base]): RNA2 =
906+
fromSpecific(new View.FlatMap(this, f))
907+
}
908+
~~~
909+
{% endtab %}
910+
{% tab 'Scala 3' for=RNA2_1 %}
911+
~~~ scala
912+
import scala.collection.{ View, mutable }
913+
import scala.collection.immutable.{ IndexedSeq, IndexedSeqOps }
914+
915+
final class RNA2 private (val groups: Array[Int], val length: Int)
916+
extends IndexedSeq[Base], IndexedSeqOps[Base, IndexedSeq, RNA2]:
917+
918+
import RNA2.*
919+
920+
def apply(idx: Int): Base = // as before
921+
override protected def fromSpecific(coll: IterableOnce[Base]): RNA2 = // as before
922+
override protected def newSpecificBuilder: mutable.Builder[Base, RNA2] = // as before
923+
override def empty: RNA2 = // as before
924+
override def className = "RNA2"
925+
926+
// Overloading of `appended`, `prepended`, `appendedAll`,
927+
// `prependedAll`, `map`, `flatMap` and `concat` to return an `RNA2`
928+
// when possible
929+
def concat(suffix: IterableOnce[Base]): RNA2 =
930+
fromSpecific(iterator ++ suffix.iterator)
931+
// symbolic alias for `concat`
932+
inline final def ++ (suffix: IterableOnce[Base]): RNA2 = concat(suffix)
933+
def appended(base: Base): RNA2 =
934+
fromSpecific(View.Appended(this, base))
935+
def appendedAll(suffix: IterableOnce[Base]): RNA2 =
936+
concat(suffix)
937+
def prepended(base: Base): RNA2 =
938+
fromSpecific(View.Prepended(base, this))
939+
def prependedAll(prefix: IterableOnce[Base]): RNA2 =
940+
fromSpecific(prefix.iterator ++ iterator)
941+
def map(f: Base => Base): RNA2 =
942+
fromSpecific(View.Map(this, f))
943+
def flatMap(f: Base => IterableOnce[Base]): RNA2 =
944+
fromSpecific(View.FlatMap(this, f))
945+
end RNA2
946+
~~~
947+
{% endtab %}
948+
{% endtabs %}
611949

612950
To address this shortcoming, you need to overload the methods that
613951
return an `IndexedSeq[B]` for the case where `B` is known to be `Base`,
@@ -622,19 +960,40 @@ collection is strict, we could take advantage of the better performance offered
622960
in transformation operations.
623961
Also, if we try to convert an `Iterable[Base]` into an `RNA2` it fails:
624962

625-
~~~
963+
{% tabs RNA2_2 class=tabs-scala-version %}
964+
{% tab 'Scala 2' for=RNA2_2 %}
965+
~~~scala
626966
scala> val bases: Iterable[Base] = List(A, U, C, C)
627-
bases: Iterable[Base] = List(A, U, C, C)
967+
val bases: Iterable[Base] = List(A, U, C, C)
628968

629969
scala> bases.to(RNA2)
630970
^
631971
error: type mismatch;
632972
found : RNA2.type
633973
required: scala.collection.Factory[Base,?]
634974
~~~
975+
{% endtab %}
976+
{% tab 'Scala 3' for=RNA2_2 %}
977+
~~~scala
978+
scala> val bases: Iterable[Base] = List(A, U, C, C)
979+
val bases: Iterable[Base] = List(A, U, C, C)
980+
981+
scala> bases.to(RNA2)
982+
-- [E007] Type Mismatch Error: -------------------------------------------------
983+
1 |bases.to(RNA2)
984+
| ^^^^
985+
| Found: RNA2.type
986+
| Required: scala.collection.Factory[Base, Any]
987+
|
988+
| longer explanation available when compiling with `-explain`
989+
~~~
990+
{% endtab %}
991+
{% endtabs %}
635992

636993
### Final version of RNA strands class ###
637994

995+
{% tabs RNA_1 class=tabs-scala-version %}
996+
{% tab 'Scala 2' for=RNA_1 %}
638997
~~~ scala
639998
import scala.collection.{ AbstractIterator, SpecificIterableFactory, StrictOptimizedSeqOps, View, mutable }
640999
import scala.collection.immutable.{ IndexedSeq, IndexedSeqOps }
@@ -723,6 +1082,94 @@ object RNA extends SpecificIterableFactory[Base, RNA] {
7231082
}
7241083
}
7251084
~~~
1085+
{% endtab %}
1086+
{% tab 'Scala 3' for=RNA_1 %}
1087+
~~~ scala
1088+
import scala.collection.{ AbstractIterator, SpecificIterableFactory, StrictOptimizedSeqOps, View, mutable }
1089+
import scala.collection.immutable.{ IndexedSeq, IndexedSeqOps }
1090+
1091+
final class RNA private
1092+
( val groups: Array[Int],
1093+
val length: Int
1094+
) extends IndexedSeq[Base],
1095+
IndexedSeqOps[Base, IndexedSeq, RNA],
1096+
StrictOptimizedSeqOps[Base, IndexedSeq, RNA]:
1097+
rna =>
1098+
1099+
import RNA.*
1100+
1101+
// Mandatory implementation of `apply` in `IndexedSeqOps`
1102+
def apply(idx: Int): Base =
1103+
if idx < 0 || length <= idx then
1104+
throw new IndexOutOfBoundsException
1105+
Base.fromInt(groups(idx / N) >> (idx % N * S) & M)
1106+
1107+
// Mandatory overrides of `fromSpecific`, `newSpecificBuilder`,
1108+
// and `empty`, from `IterableOps`
1109+
override protected def fromSpecific(coll: IterableOnce[Base]): RNA =
1110+
RNA.fromSpecific(coll)
1111+
override protected def newSpecificBuilder: mutable.Builder[Base, RNA] =
1112+
RNA.newBuilder
1113+
override def empty: RNA = RNA.empty
1114+
1115+
// Overloading of `appended`, `prepended`, `appendedAll`, `prependedAll`,
1116+
// `map`, `flatMap` and `concat` to return an `RNA` when possible
1117+
def concat(suffix: IterableOnce[Base]): RNA =
1118+
strictOptimizedConcat(suffix, newSpecificBuilder)
1119+
inline final def ++ (suffix: IterableOnce[Base]): RNA = concat(suffix)
1120+
def appended(base: Base): RNA =
1121+
(newSpecificBuilder ++= this += base).result()
1122+
def appendedAll(suffix: Iterable[Base]): RNA =
1123+
strictOptimizedConcat(suffix, newSpecificBuilder)
1124+
def prepended(base: Base): RNA =
1125+
(newSpecificBuilder += base ++= this).result()
1126+
def prependedAll(prefix: Iterable[Base]): RNA =
1127+
(newSpecificBuilder ++= prefix ++= this).result()
1128+
def map(f: Base => Base): RNA =
1129+
strictOptimizedMap(newSpecificBuilder, f)
1130+
def flatMap(f: Base => IterableOnce[Base]): RNA =
1131+
strictOptimizedFlatMap(newSpecificBuilder, f)
1132+
1133+
// Optional re-implementation of iterator,
1134+
// to make it more efficient.
1135+
override def iterator: Iterator[Base] = new AbstractIterator[Base]:
1136+
private var i = 0
1137+
private var b = 0
1138+
def hasNext: Boolean = i < rna.length
1139+
def next(): Base =
1140+
b = if i % N == 0 then groups(i / N) else b >>> S
1141+
i += 1
1142+
Base.fromInt(b & M)
1143+
1144+
override def className = "RNA"
1145+
end RNA
1146+
1147+
object RNA extends SpecificIterableFactory[Base, RNA]:
1148+
1149+
private val S = 2 // number of bits in group
1150+
private val M = (1 << S) - 1 // bitmask to isolate a group
1151+
private val N = 32 / S // number of groups in an Int
1152+
1153+
def fromSeq(buf: collection.Seq[Base]): RNA =
1154+
val groups = new Array[Int]((buf.length + N - 1) / N)
1155+
for i <- 0 until buf.length do
1156+
groups(i / N) |= Base.toInt(buf(i)) << (i % N * S)
1157+
new RNA(groups, buf.length)
1158+
1159+
// Mandatory factory methods: `empty`, `newBuilder`
1160+
// and `fromSpecific`
1161+
def empty: RNA = fromSeq(Seq.empty)
1162+
1163+
def newBuilder: mutable.Builder[Base, RNA] =
1164+
mutable.ArrayBuffer.newBuilder[Base].mapResult(fromSeq)
1165+
1166+
def fromSpecific(it: IterableOnce[Base]): RNA = it match
1167+
case seq: collection.Seq[Base] => fromSeq(seq)
1168+
case _ => fromSeq(mutable.ArrayBuffer.from(it))
1169+
end RNA
1170+
~~~
1171+
{% endtab %}
1172+
{% endtabs %}
7261173

7271174
The final [`RNA` class](#final-version-of-rna-strands-class):
7281175

@@ -793,17 +1240,35 @@ of a map that's implemented as a Patricia trie. We call the map a
7931240
selects a submap of all keys starting with a given prefix. We'll first
7941241
define a prefix map with the keys shown in the running example:
7951242

796-
scala> val m = PrefixMap("abc" -> 0, "abd" -> 1, "al" -> 2,
797-
"all" -> 3, "xy" -> 4)
798-
m: PrefixMap[Int] = PrefixMap((abc,0), (abd,1), (al,2), (all,3), (xy,4))
1243+
{% tabs prefixMap_1 %}
1244+
{% tab 'Scala 2 and 3' for=prefixMap_1 %}
1245+
1246+
```scala
1247+
scala> val m = PrefixMap("abc" -> 0, "abd" -> 1, "al" -> 2,
1248+
"all" -> 3, "xy" -> 4)
1249+
val m: PrefixMap[Int] = PrefixMap((abc,0), (abd,1), (al,2), (all,3), (xy,4))
1250+
```
1251+
1252+
{% endtab %}
1253+
{% endtabs %}
7991254

8001255
Then calling `withPrefix` on `m` will yield another prefix map:
8011256

802-
scala> m withPrefix "a"
803-
res14: PrefixMap[Int] = PrefixMap((bc,0), (bd,1), (l,2), (ll,3))
1257+
{% tabs prefixMap_2 %}
1258+
{% tab 'Scala 2 and 3' for=prefixMap_2 %}
1259+
1260+
```scala
1261+
scala> m.withPrefix("a")
1262+
val res14: PrefixMap[Int] = PrefixMap((bc,0), (bd,1), (l,2), (ll,3))
1263+
```
1264+
1265+
{% endtab %}
1266+
{% endtabs %}
8041267

8051268
### Patricia trie implementation ###
8061269

1270+
{% tabs prefixMap_3 class=tabs-scala-version %}
1271+
{% tab 'Scala 2' for=prefixMap_3 %}
8071272
~~~ scala
8081273
import scala.collection._
8091274
import scala.collection.mutable.{ GrowableBuilder, Builder }
@@ -818,18 +1283,18 @@ class PrefixMap[A]
8181283

8191284
def get(s: String): Option[A] =
8201285
if (s.isEmpty) value
821-
else suffixes get (s(0)) flatMap (_.get(s substring 1))
1286+
else suffixes.get(s(0)).flatMap(_.get(s.substring(1)))
8221287

8231288
def withPrefix(s: String): PrefixMap[A] =
8241289
if (s.isEmpty) this
8251290
else {
8261291
val leading = s(0)
827-
suffixes get leading match {
1292+
suffixes.get(leading) match {
8281293
case None =>
8291294
suffixes = suffixes + (leading -> empty)
8301295
case _ =>
8311296
}
832-
suffixes(leading) withPrefix (s substring 1)
1297+
suffixes(leading).withPrefix(s.substring(1))
8331298
}
8341299

8351300
def iterator: Iterator[(String, A)] =
@@ -844,7 +1309,7 @@ class PrefixMap[A]
8441309

8451310
def subtractOne(s: String): this.type = {
8461311
if (s.isEmpty) { val prev = value; value = None; prev }
847-
else suffixes get (s(0)) flatMap (_.remove(s substring 1))
1312+
else suffixes.get(s(0)).flatMap(_.remove(s.substring(1)))
8481313
this
8491314
}
8501315

@@ -864,7 +1329,7 @@ class PrefixMap[A]
8641329
// Members declared in scala.collection.IterableOps
8651330
override protected def fromSpecific(coll: IterableOnce[(String, A)]): PrefixMap[A] = PrefixMap.from(coll)
8661331
override protected def newSpecificBuilder: mutable.Builder[(String, A), PrefixMap[A]] = PrefixMap.newBuilder
867-
1332+
8681333
override def className = "PrefixMap"
8691334
}
8701335

@@ -892,6 +1357,91 @@ object PrefixMap {
8921357

8931358
}
8941359
~~~
1360+
{% endtab %}
1361+
{% tab 'Scala 3' for=prefixMap_3 %}
1362+
~~~ scala
1363+
import scala.collection.*
1364+
import scala.collection.mutable.{ GrowableBuilder, Builder }
1365+
1366+
class PrefixMap[A]
1367+
extends mutable.Map[String, A],
1368+
mutable.MapOps[String, A, mutable.Map, PrefixMap[A]],
1369+
StrictOptimizedIterableOps[(String, A), mutable.Iterable, PrefixMap[A]]:
1370+
1371+
private var suffixes: immutable.Map[Char, PrefixMap[A]] = immutable.Map.empty
1372+
private var value: Option[A] = None
1373+
1374+
def get(s: String): Option[A] =
1375+
if s.isEmpty then value
1376+
else suffixes.get(s(0)).flatMap(_.get(s.substring(1)))
1377+
1378+
def withPrefix(s: String): PrefixMap[A] =
1379+
if s.isEmpty then this
1380+
else
1381+
val leading = s(0)
1382+
suffixes.get(leading) match
1383+
case None =>
1384+
suffixes = suffixes + (leading -> empty)
1385+
case _ =>
1386+
suffixes(leading).withPrefix(s.substring(1))
1387+
1388+
def iterator: Iterator[(String, A)] =
1389+
(for v <- value.iterator yield ("", v)) ++
1390+
(for (chr, m) <- suffixes.iterator
1391+
(s, v) <- m.iterator yield (chr +: s, v))
1392+
1393+
def addOne(kv: (String, A)): this.type =
1394+
withPrefix(kv._1).value = Some(kv._2)
1395+
this
1396+
1397+
def subtractOne(s: String): this.type =
1398+
if s.isEmpty then { val prev = value; value = None; prev }
1399+
else suffixes.get(s(0)).flatMap(_.remove(s.substring(1)))
1400+
this
1401+
1402+
// Overloading of transformation methods that should return a PrefixMap
1403+
def map[B](f: ((String, A)) => (String, B)): PrefixMap[B] =
1404+
strictOptimizedMap(PrefixMap.newBuilder, f)
1405+
def flatMap[B](f: ((String, A)) => IterableOnce[(String, B)]): PrefixMap[B] =
1406+
strictOptimizedFlatMap(PrefixMap.newBuilder, f)
1407+
1408+
// Override `concat` and `empty` methods to refine their return type
1409+
override def concat[B >: A](suffix: IterableOnce[(String, B)]): PrefixMap[B] =
1410+
strictOptimizedConcat(suffix, PrefixMap.newBuilder)
1411+
override def empty: PrefixMap[A] = PrefixMap()
1412+
1413+
// Members declared in scala.collection.mutable.Clearable
1414+
override def clear(): Unit = suffixes = immutable.Map.empty
1415+
// Members declared in scala.collection.IterableOps
1416+
override protected def fromSpecific(coll: IterableOnce[(String, A)]): PrefixMap[A] = PrefixMap.from(coll)
1417+
override protected def newSpecificBuilder: mutable.Builder[(String, A), PrefixMap[A]] = PrefixMap.newBuilder
1418+
1419+
override def className = "PrefixMap"
1420+
end PrefixMap
1421+
1422+
object PrefixMap:
1423+
def empty[A] = new PrefixMap[A]
1424+
1425+
def from[A](source: IterableOnce[(String, A)]): PrefixMap[A] =
1426+
source match
1427+
case pm: PrefixMap[A @unchecked] => pm
1428+
case _ => (newBuilder ++= source).result()
1429+
1430+
def apply[A](kvs: (String, A)*): PrefixMap[A] = from(kvs)
1431+
1432+
def newBuilder[A]: mutable.Builder[(String, A), PrefixMap[A]] =
1433+
mutable.GrowableBuilder[(String, A), PrefixMap[A]](empty)
1434+
1435+
import scala.language.implicitConversions
1436+
1437+
implicit def toFactory[A](self: this.type): Factory[(String, A), PrefixMap[A]] =
1438+
new Factory[(String, A), PrefixMap[A]]:
1439+
def fromSpecific(it: IterableOnce[(String, A)]): PrefixMap[A] = self.from(it)
1440+
def newBuilder: mutable.Builder[(String, A), PrefixMap[A]] = self.newBuilder
1441+
end PrefixMap
1442+
~~~
1443+
{% endtab %}
1444+
{% endtabs %}
8951445

8961446
The previous listing shows the definition of `PrefixMap`. The map has
8971447
keys of type `String` and the values are of parametric type `A`. It extends
@@ -984,11 +1534,19 @@ present for all other collections in Scala's collection framework, so
9841534
it makes sense to define them here, too. With the two methods, you can
9851535
write `PrefixMap` literals like you do for any other collection:
9861536

987-
scala> PrefixMap("hello" -> 5, "hi" -> 2)
988-
res0: PrefixMap[Int] = PrefixMap(hello -> 5, hi -> 2)
1537+
{% tabs prefixMap_4 %}
1538+
{% tab 'Scala 2 and 3' for=prefixMap_4 %}
1539+
1540+
```scala
1541+
scala> PrefixMap("hello" -> 5, "hi" -> 2)
1542+
val res0: PrefixMap[Int] = PrefixMap(hello -> 5, hi -> 2)
1543+
1544+
scala> res0 += "foo" -> 3
1545+
val res1: res0.type = PrefixMap(hello -> 5, hi -> 2, foo -> 3)
1546+
```
9891547

990-
scala> res0 += "foo" -> 3
991-
res1: res0.type = PrefixMap(hello -> 5, hi -> 2, foo -> 3)
1548+
{% endtab %}
1549+
{% endtabs %}
9921550

9931551
## Summary ##
9941552

0 commit comments

Comments
 (0)
Please sign in to comment.