Using Our Custom ‘Binding’ Class In for Expressions (Scala 3 Video)
Getting Close to Using bind
in for
Expressions
Now we’re at a point where we see that bind
is better than what I started with, but not as good as it can be. That is, I want to use f
, g
, and h
in a for
expression, but I can’t, because bind
is a function, and therefore it has no way to implement map
and flatMap
so it can work in a for
expression.
What to do?
Well, if we’re going to succeed we need to figure out how to create a class that does two things:
- Somehow works like
bind
- Implements
map
andflatMap
methods so it can work insidefor
expressions
A different way to write map
and flatMap
methods
I’ll do that soon, but first I need to do something else: I need to demonstrate a different way to write map
and flatMap
methods.
In the previous examples I showed how to write map
and flatMap
methods for a Sequence
class that works like a collection class. Before we solve the bind
problem I need to show how to write map
and flatMap
for classes that are more like a “wrapper” than a “collection.” I’ll do that next.
Using a “Wrapper” Class in a for
Expression
To set the groundwork for solving the problem of using bind
in a for
expression, I’m going to create a simple “wrapper” class that you can use in a for
expression. By the end of this lesson you’ll have a class that you can use like this:
val result: Wrapper[Int] = for {
a <- new Wrapper(1)
b <- new Wrapper(2)
c <- new Wrapper(3)
} yield a + b + c
To keep things simple, the Wrapper
class won’t do much; by the end of this lesson that for
expression will yield this result:
result: Wrapper[Int] = 6
But even though it will be simple, this class will demonstrate how to write map
and flatMap
methods for these kinds of classes, what I call “wrapper” classes.
It helps to think of these classes as being different than “collection” classes. As I’ve mentioned a few times, it’s better to think of them as being a wrapper around whatever it is that they wrap.
In this lesson I’ll create a wrapper around Int
. I’ll do this because a) Int
is a simple data type that’s easy to understand, and b) it lets us create map
and flatMap
methods in a manner that’s similar to what bind
is going to need.
Beginning
Following the concept of “beginning with the end in mind,” this is the solution that I’ll be able to write by the end of this lesson:
val result: Wrapper[Int] = for {
a <- new Wrapper(1)
b <- new Wrapper(2)
c <- new Wrapper(3)
} yield a + b + c
This code tells me a couple of things:
Wrapper
will be a class that takes a singleInt
constructor parameter- Because it works with multiple generators in a
for
expression,Wrapper
must implementmap
andflatMap
methods - Because
result
has the typeWrapper[Int]
, thosemap
andflatMap
functions must return that same type
Knowing these things, I can begin sketching the Wrapper
class like this:
class Wrapper[Int](value: Int) {
def map(f: Int => Int): Wrapper[Int] = ???
def flatMap(f: Int => Wrapper[Int]): Wrapper[Int] = ???
}
That’s cool, I can sketch quite a bit of the Wrapper
class just by knowing how it’s used in a for
expression. All that remains now is writing the body of the map
and flatMap
methods.
Implementing map
When you look at map
’s signature:
def map(f: Int => Int): Wrapper[Int] = ???
you can say these things about map
:
- It takes a function that transforms an
Int
to anInt
- After doing whatever it does,
map
returns aWrapper[Int]
(i.e., anInt
inside a newWrapper
)
Writing those statements as comments inside map
’s body looks like this:
def map(f: Int => Int): Wrapper[Int] = {
// apply `f` to an `Int` to get a new `Int`
// wrap the new `Int` in a `Wrapper`
}
If you’re smarter than I was when I first learned about this, you might guess that those comments translate into code that looks like this:
def map(f: Int => Int): Wrapper[Int] = {
// apply `f` to an `Int` to get a new `Int`
val newInt = f(SOME_INT)
// wrap the new `Int` in a `Wrapper`
new Wrapper(newInt)
}
As it turns out, map
is a very literal interpretation of those two comments. The only question is, what is SOME_INT
? Where does it come from?
Where SOME_INT
comes from
Well, remember that when you create a new Wrapper
, you create it like this:
val x = Wrapper(1)
That’s where the Int
comes from: Wrapper
’s constructor:
class Wrapper[Int](value: Int) { ...
----------
This tells me that SOME_INT
in map
’s body should be changed to value
, which leads to this code:
def map(f: Int => Int): Wrapper[Int] = {
// apply `f` to an `Int` to get a new `Int`
val newInt = f(value)
// wrap the new `Int` in a `Wrapper`
new Wrapper(newInt)
}
If this feels unusual ...
Here’s what the relationship looks like visually:
(This image is shown in the video.)  If this feels unusual at this point — congratulations! Your mind is now at a point where it feels like variables shouldn’t just magically appear inside your functions. This is a good thing.
What’s happening here is that you’re using value
just like you would in OOP code. It’s a constructor parameter that you’re using inside map
. This is part of Scala’s “functional objects” paradigm.
In another programming language you might write map
in a separate WrapperUtils
class, where value
would be passed in explicitly:
class WrapperUtils {
def map(value: Int, f: Int => Int): Wrapper[Int] = {
val newInt = f(value)
new Wrapper(newInt)
}
}
But in Scala’s “functional objects” approach, you can access value
implicitly. Technically this violates my rule that for pure functions, “Output depends only on input,” but when you use the functional objects coding style, this is the one case where you are allowed to implicitly access other fields in a function in your class.
If you’ve heard the term “closure” before, yes, this is essentially a closure.
Here’s the source code for the current Wrapper
class:
The current Wrapper
class
Getting back to the problem at hand, here’s the source code for the Wrapper
class with its new map
method:
class Wrapper[Int](value: Int) {
def map(f: Int => Int): Wrapper[Int] = {
// apply `f` to an `Int` to get a new `Int`
val newInt = f(value)
// wrap the new `Int` in a `Wrapper`
new Wrapper(newInt)
}
def flatMap(f: Int => Wrapper[Int]): Wrapper[Int] = ???
}
If this is confusing ...
If this is confusing, remember that it’s the same way that Scala’s List
class works. It takes some parameters in its constructor:
val x = List(1,2,3)
and then its map
method operates on those parameters:
x.map(_ * 2) // yields `List(2,4,6)`
Wrapper
works the same way:
val x = new Wrapper(1)
x.map(_ * 2)
Testing map
If you paste the current Wrapper
class into the Scala REPL and then run those last two lines of code, you’ll see this output:
scala> val x = new Wrapper(1)
x: Wrapper[Int] = 1
scala> x.map(_ * 2)
res0: Wrapper[Int] = 2
Very cool.
As a final note, remember that implementing a map
method in a class like Wrapper
lets you use one generator in a for
expression, so this code also works right now:
scala> for { i <- x } yield i * 2
res1: Wrapper[Int] = 2
So far, so good. Now let’s create a flatMap
method in Wrapper
.
Implementing flatMap
Let’s follow the same thought process to see if we can create flatMap
’s body. First, when you look at flatMap
’s signature:
def flatMap(f: Int => Wrapper[Int]): Wrapper[Int] = {
you can say these things about flatMap
:
- It takes a function that transforms an
Int
to aWrapper[Int]
- After doing whatever it does,
flatMap
returns aWrapper[Int]
When I write those statements as comments inside flatMap
’s signature, I get this:
def flatMap(f: Int => Wrapper[Int]): Wrapper[Int] = {
// apply `f` to an `Int` to get a `Wrapper[Int]`
// return a new `Wrapper[Int]`
}
flatMap
always seems harder to grok than map
, but if I’m reading those comments right, the flatMap
solution is simpler than map
. Here’s what the solution looks like:
def flatMap(f: Int => Wrapper[Int]): Wrapper[Int] = {
// apply `f` to an `Int` to get a `Wrapper[Int]`
val newValue = f(value)
// return a new `Wrapper[Int]`
newValue
}
It turns out that flatMap
is so simple that you can reduce that code to this:
def flatMap(f: Int => Wrapper[Int]): Wrapper[Int] = {
f(value)
}
and then this:
def flatMap(f: Int => Wrapper[Int]): Wrapper[Int] = f(value)
For a “wrapper” class like the Wrapper
, it turns out that flatMap
is extremely simple: It just applies the function it’s given (f
) to the value it wraps (value
).
The complete source code
Here’s the complete source code for the Wrapper
class:
class Wrapper[Int](value: Int) {
def map(f: Int => Int): Wrapper[Int] = {
val newValue = f(value)
new Wrapper(newValue)
}
def flatMap(f: Int => Wrapper[Int]): Wrapper[Int] = f(value)
override def toString = value.toString
}
If you paste this Wrapper
class into the REPL and then run this for
expression:
val result: Wrapper[Int] = for {
a <- new Wrapper(1)
b <- new Wrapper(2)
c <- new Wrapper(3)
} yield a + b + c
you’ll see the REPL show this result:
result: Wrapper[Int] = 6
Kind of a big step
Congratulations, you just implemented map
and flatMap
methods for a non-collection class, i.e., a type of class that I call a “wrapper.”
If this seems like a minor achievement, well, it’s actually kind of a big deal. You’re about to see that this is an important step that will soon let you use the bind
approach in a for
expression. (And that’s also going to be kind of a big deal.)
Update: All of my new videos are now on
LearnScala.dev