home search about rss feed twitter ko-fi

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:

  1. Somehow works like bind
  2. Implements map and flatMap methods so it can work inside for 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:

  1. Wrapper will be a class that takes a single Int constructor parameter
  2. Because it works with multiple generators in a for expression, Wrapper must implement map and flatMap methods
  3. Because result has the type Wrapper[Int], those map and flatMap 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 an Int
  • After doing whatever it does, map returns a Wrapper[Int] (i.e., an Int inside a new Wrapper)

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 a Wrapper[Int]
  • After doing whatever it does, flatMap returns a Wrapper[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.)