Sequence: Writing 'map' to Work as a Single Generator (Scala 3 Video)
Getting Sequence
to work as a generator in a simple for
loop was cool, but does adding foreach
let Sequence
also work when I add yield
? Let’s see.
When I paste this code into the REPL:
val ints = Sequence(1,2,3)
for {
i <- ints
} yield i*2
I see this error message:
scala> for {
| i <- ints
| } yield i*2
<console>:15: error: value map is not a member of Sequence[Int]
i <- ints
^
Sadly, Sequence
won’t currently work with for
/yield
, but again the REPL tells us why:
error: value map is not a member of Sequence[Int]
That error tells us that Sequence
needs a map
method for this to work. Great — let’s create one.
Adding a map
method to Sequence
Again I’m going to cheat to create a simple solution, this time using ArrayBuffer
’s map
method inside Sequence
’s map
method:
def map[B](f: A => B): Sequence[B] = {
val abMap: ArrayBuffer[B] = elems.map(f)
Sequence(abMap: _*)
}
This map
method does the following:
- It takes a function input parameter that transforms a type
A
to a typeB
. - When it’s finished,
map
returns aSequence[B]
. - In the first line of the function I show
abMap: ArrayBuffer[B]
to be clear thatelems.map(f)
returns anArrayBuffer
. As usual, showing the type isn’t necessary, but I think it helps to make this step clear. - In the second line inside the function I use the
:_*
syntax to create a newSequence
and return it.
About the :_*
syntax
If you haven’t seen the abMap: _*
syntax before, the :_*
part of the code is a way to adapt a collection to work with a varargs parameter. Recall that the Sequence
constructor is defined to take a varags parameter:
For more information on this syntax, see my tutorial, Scala’s missing splat operator.
The complete Sequence
class
This is what the Sequence
class looks like when I add the map
method to it:
case class Sequence[A](initialElems: A*) {
private val elems = scala.collection.mutable.ArrayBuffer[A]()
// initialize
elems ++= initialElems
def map[B](f: A => B): Sequence[B] = {
val abMap = elems.map(f)
new Sequence(abMap: _*)
}
def foreach(block: A => Unit): Unit = {
elems.foreach(block)
}
}
Does for/yield work now?
Now when I go back and try to use the for
/yield
expression I showed earlier, I find that it compiles and runs just fine:
scala> val ints = Sequence(1,2,3)
ints: Sequence[Int] = Sequence(WrappedArray(1, 2, 3))
scala> for {
| i <- ints
| } yield i*2
res0: Sequence[Int] = Sequence(ArrayBuffer(2, 4, 6))
An important point
One point I need to make clear is that this for
/yield
expression works solely because of the map
method; it has nothing to do with the foreach
method.
You can demonstrate this in at least two ways. First, if you remove the foreach
method from the Sequence
class you’ll see that this for
expression still works.
Second, if you create a little test class with this code in it, and then compile it with scalac -Xprint:parse
, you’ll see that the Scala compiler converts this for
expression:
for {
i <- ints
} yield i*2
into this map
expression:
ints.map(((i) => i.$times(2)))
To be very clear, creating a foreach
in Sequence
enables this for
loop:
for (i <- ints) println(i)
and defining a map
method in Sequence
enables this for
expression:
for {
i <- ints
} yield i*2
Summary
I can summarize what I accomplished in this lesson and the previous lesson with these lines of code:
// (1) works because `foreach` is defined
for (p <- peeps) println(p)
// (2) `yield` works because `map` is defined
val res: Sequence[Int] = for {
i <- ints
} yield i * 2
res.foreach(println) // verify the result
What’s next?
This is a good start. Next up, I’ll modify Sequence
so I can use it with filtering clauses in for
expressions.
Update: All of my new videos are now on
LearnScala.dev