The best way I know to demonstrate how the Scala for expression works is for us to build our own collection class.
To keep things simple I’m going to create a custom class as a “wrapper” around an existing Scala collection class. The reason for this is that I want you to focus on the effects that writing map, flatMap, withFilter, and foreach methods have on how the class works in a for expression — not on writing the gory internals of a collection class.
A Sequence class
I always like to “begin with the end in mind” and picture how I want to use a class before I create it — i.e., its API — and to that end, this is how I want to use a custom class that I’m going to name Sequence:
val strings = Sequence("one", "two")
val nums = Sequence(1, 2, 3, 4, 5)
val peeps = Sequence(
Person("Bert"),
Person("Ernie"),
Person("Grover")
)
From that code you can see that Sequence will be able to work with generic data types: it will be able to contain a series of String, Int, Person, and other data types.
Given those lines of code, you can infer some initial requirements about the Sequence class:
Sequenceeither needs to be acaseclass, or it needs to have a companion object that implements anapplymethod, because I want to be able to create newSequenceinstances without needing thenewkeyword.- Because the class will be used as a container for generic elements, I’ll define the class to take a generic type.
- Because
Sequenceinstances can be created with a variable number of initial elements, theSequenceclass constructor will be defined to accept a “varargs” parameter.
I’ll create this class in a series of steps over the next several lessons.
Create a case class named Sequence
The first step is to create a class named Sequence. I’m going to make it a case class so I can write code like this:
val strings = Sequence(1, 2, 3)
If I didn’t use a case class (or an apply method in a companion object) I’d have to write “new Sequence”, like this:
val strings = new Sequence("one", "two")
Therefore, I start by creating a case class named Sequence:
case class Sequence ...
Sequence will be a container for generic elements
Next, I know that I want Sequence to contain elements of different types, so I expand that definition to say that Sequence will be a container of generic types:
case class Sequence[A] ...
Sequence’s constructor will take a variable number of input parameters
Next, I know that the Sequence constructor will have one parameter, and that parameter can be assigned to a variable number of elements, so I expand the definition to this:
case class Sequence[A](initialElems: A*) ...
If you’re not familiar with using a varargs parameter, the * after the A is what lets you pass a variable number of elements into the Sequence constructor:
val a = Sequence(1,2)
val b = Sequence(1,2,3)
val c = Sequence('a', 'b', 'c', 'd', 'e')
Later in the Sequence class code you’ll see how to handle a variable number of input elements.
If you have a hard time with generics
If you have a hard time using generic types, it can help to remove the generic type A and use Int instead:
case class Sequence(initialElems: Int*) ...
With this code you only have to think about Sequence being a container for integers, so combined with the varargs constructor parameter, new instances of Sequence can be created like this:
val a = Sequence(1,2)
val b = Sequence(3,5,7,11,13,17,23)
Feel free to write your own code using Int rather than A, though I’ll use A in the rest of this lesson:
case class Sequence[A](initialElems: A*) ...
Sequence will be backed by a Scala collection class
As I mentioned at the beginning of this lesson, to keep this code from getting too complicated I’m going to implement Sequence as a wrapper around a Scala collection class. I originally wrote this lesson using a custom linked list class, but with that approach there was a lot of code that was unrelated to for expressions, so I opted to take this simpler approach.
The following code shows what I have in mind:
case class Sequence[A](initialElems: A*) {
// this is a book, don't do this at home
private val elems = scala.collection.mutable.ArrayBuffer[A]()
// initialize
elems ++= initialElems
}
I make ArrayBuffer private in this code so no consumers of my class can see how it’s implemented.
Scala constructors
If you haven’t use a Scala constructor in a while, remember that everything inside the body of a class that isn’t a method is executed when a new instance of the class is created. Therefore, this line of code:
elems ++= initialElems
is executed when a new class instance is created. For example, when you create a new Sequence like this:
val ints = Sequence(1, 2, 3)
the int values 1, 2, and 3 are added to the elems variable when the class is first created.
If you need to brush up on how Scala class constructors work, see Chapter 4 of the Scala Cookbook.
About ++=
If you’re not used to the ++= method, this line of code:
elems ++= initialElems
works just like this for loop:
for {
e <- initialElems
} elems += e
Summary
If you want to test this code before moving on to the next lesson, paste it into the Scala REPL and then create new sequences like these:
val strings = Sequence("a", "b", "c")
val nums = Sequence(1, 2, 3, 4, 5)
you’ll see that they’re created properly:
scala> val strings = Sequence("a", "b", "c")
strings: Sequence[String] = Sequence(WrappedArray(a, b, c))
scala> val nums = Sequence(1, 2, 3, 4, 5)
nums: Sequence[Int] = Sequence(WrappedArray(1, 2, 3, 4, 5))
Notice that strings has the type Sequence[String], and nums has the type Sequence[Int]. That’s generics at work.
Don’t worry about the data on the right side of the
=showingSequence(WrappedArray(...)). That’s just an artifact of taking a varargs constructor parameter.
What’s next
Now that I have a simple Sequence class to work with, I’ll start to make it work with for expressions in the next lesson.