Creating A Custom Sequence Class (Scala 3 Video)
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:
Sequence
either needs to be acase
class, or it needs to have a companion object that implements anapply
method, because I want to be able to create newSequence
instances without needing thenew
keyword.- Because the class will be used as a container for generic elements, I’ll define the class to take a generic type.
- Because
Sequence
instances can be created with a variable number of initial elements, theSequence
class 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.
See also
Update: All of my new videos are now on
LearnScala.dev