home search about rss feed twitter ko-fi

The “Bind” Concept (Scala 3 Video)

Because Scala supports higher-order functions (HOFs), you can improve this situation by writing a bind function to glue f and g together a little more easily. For instance, with a properly written bind function you can write code like this to glue together f, g, and h (a new function that has the same signature as f and g):

val fResult = f(100)
val gResult = bind(g, fResult)
val hResult = bind(h, gResult)

While this code might not be beautiful just yet, it’s certainly better and less error prone than the code I had at the end of the previous lesson.

In this lesson I’ll show how to write a bind function to make this work.

Problem statement

Before beginning, let me clearly define the problem. Given three functions with these signatures:

def f(a: Int): (Int, String) = ???
def g(a: Int): (Int, String) = ???
def h(a: Int): (Int, String) = ???

I want to write a bind function that works like this:

val fResult = f(100)
val gResult = bind(g, fResult)
val hResult = bind(h, gResult)

If you think you know how to do this, step away from the book and start typing in your favorite IDE. Otherwise, read on.

Writing bind’s type signature

Let’s solve this problem using the function-writing strategy I introduced early in the book: By writing bind’s function signature before you do anything else.

bind’s input parameters

To understand what bind’s signature needs to be, look at this line:

val gResult = bind(g, fResult)

This tells you that bind takes two parameters:

  1. The first parameter is the function g. As I just showed, g has the type signature (a: Int): (Int, String) (or, (a: Int) => (Int, String), if you prefer).
  2. The second parameter is fResult, which is the output of f. f’s signature tells us that fResult’s type is (Int, String) (i.e., a Tuple2[Int, String]).

This tells you that bind’s input parameters look like this:

def bind(fun: (Int) => (Int, String),
         tup: Tuple2[Int, String]) ...

Now all you need is bind’s return type.

bind’s return type

By looking at these two lines of code:

val gResult = bind(g, fResult)
val hResult = bind(h, gResult)

you can see that gResult will have the type of whatever bind returns. Because you know that a) fResult in the first line has the type (Int, String), and b) gResult must have the same type as fResult, you can deduce that c) bind must have this same return type: (Int, String).

Therefore, the complete type signature for bind must be this:

def bind(fun: (Int) => (Int, String),
         tup: (Int, String) ): (Int, String) = ???

Now all you have to do is implement the body for bind.

Note: A Tuple2 can also been written as Tuple2[Int, String].

Writing bind’s body

As for writing bind’s body, all it does is automate the process I showed at the end of the previous lesson. There I showed this code:

val (fInt, fString) = f(100)

Imagine that this tuple result will be used as input to bind, so bind will receive:

  • The function g as its first parameter
  • The tuple (fInt, fString) as its second parameter

Now, where in the previous lesson I had these lines of code:

val (gInt, gString) = g(fInt)
val debug = fString + gString

you can imagine them being replaced by this bind call:

val (gInt, gString) = bind(g, (fInt, fString))

By looking at this you can say that bind’s algorithm should be:

  1. Apply the function you’re given (g) to the Int you’re given (fInt). This creates an (Int, String) ((gInt, gString) in this example).
  2. Append the new string (gString) to the string you were given (fString).
  3. Return the new Int (gInt) and the merged String (fString + gString) as the result.

Following that algorithm, a first attempt at bind looks like this:

def bind(fun: (Int) => (Int, String),
         tup: Tuple2[Int, String]): (Int, String) =
{
    val givenInt = tup._1
    val givenString = tup._2

    // apply the given function to the given int
    val (intResult, stringResult) = fun(givenInt)

    // append `stringResult` to the given string
    val newString = givenString + stringResult

    // return the new int and string
    (intResult, newString)
}

Once you’re comfortable with that code, you can reduce bind until it looks like this:

def bind(fun: (Int) => (Int, String),
         tup: Tuple2[Int, String]): (Int, String) =
{
    val (intResult, stringResult) = fun(tup._1)
    (intResult, tup._2 + stringResult)
}

(Or you can keep the original version, if you prefer.)

A complete example

Here’s the source code for a complete example that shows how f, g, h, and bind work together:

object Step3Bind extends App {

    def f(a: Int): (Int, String) = {
        val result = a * 2
        (result, s"\nf result: $result.")
    }

    def g(a: Int): (Int, String) = {
        val result = a * 3
        (result, s"\ng result: $result.")
    }

    def h(a: Int): (Int, String) = {
        val result = a * 4
        (result, s"\nh result: $result.")
    }

    // bind, a HOF
    def bind(fun: (Int) => (Int, String),
             tup: Tuple2[Int, String]): (Int, String) =
    {
        val (intResult, stringResult) = fun(tup._1)
        (intResult, tup._2 + stringResult)
    }

    val fResult = f(100)
    val gResult = bind(g, fResult)
    val hResult = bind(h, gResult)

    println(s"result: ${hResult._1}, debug: ${hResult._2}")

}

Observations

What can we say about bind at this point? First, a few good things:

  • It’s a useful higher-order function (HOF)
  • It gives us a way to bind/glue the functions f, g, and h
  • It’s simpler and less error-prone than the code at the end of the previous lesson

If there’s anything bad to say about bind, it’s that it looks like it’s dying to be used in a for expression, but because bind doesn’t have methods like map and flatMap, it won’t work that way.

For example, wouldn’t it be cool if you could write code that looked like this:

val finalResult = for {
    fResult <- f(100)
    gResult <- g(fResult)
    hResult <- h(gResult)
} yield hResult