home search about rss feed twitter ko-fi

Starting To Glue Functions Together (Scala 3 Video)

As I mentioned at the beginning of this book, writing pure functions isn’t hard. Just make sure that output depends only on input, and you’re in good shape.

The hard part of functional programming involves how you glue together all of your pure functions to make a complete application. Because this process feels like you’re writing “glue” code, I refer to this process as “gluing,” and as I learned, a more technical term for this is called “binding.” This process is what the remainder of this book is about.

As you might have guessed from the emphasis of the previous lessons, in Scala/FP this binding process involves the use of for expressions.

Life is good when the output of one function matches the input of another

To set the groundwork for where we’re going, let’s start with a simplified version of a problem you’ll run into in the real world.

Imagine that you have two functions named f and g, and they both a) take an Int as an input parameter, and b) return an Int as their result. Therefore, their function signatures look like this:

def f(a: Int): Int = ???
def g(a: Int): Int = ???

A nice thing about these functions is that because the output of f is an Int, it perfectly matches the input of g, which takes an Int parameter.

(Image is shown in the video.)

Because the output of f is a perfect match to the input of g, you can write this code:

val x = g(f(100))

Here’s a complete example that demonstrates this:

def f(a: Int): Int = a * 2
def g(a: Int): Int = a * 3
val x = g(f(100))
println(x)

Because f(100) is 200 and g of 200 is 600, this code prints 600. So far, so good.

A new problem

Next, imagine a slightly more complicated set of requirements where f and g still take an Int as input, but now they return a String in addition to an Int. With this change their signatures look like this:

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

One possible use case for this situation is to imagine that f and g are functions in an application that uses a rules engine or artificial intelligence (AI). In this situation, not only do you want to know their result (the Int), you also want to know how they came up with their answer, i.e., the logical explanation in the form of a String.

Think of writing an application to determine the shortest route from Point A to Point Z. A function might return a log message like, “I chose B as the next step because it was closer than C or D.”

While it’s nice to get a log message back from the functions, this also creates a problem: I can no longer use the output of f as the input to g. In this new world, g takes an Int input parameter, but f now returns (Int, String) (a Tuple2[Int, String]).

(Image is shown in the video.)

When you really want to plug the output of f into the input of g, what can you do?

Solving the problem manually

Let’s look at how we’d solve this problem manually. First, I’d get the result from f:

val (fInt, fString) = f(100)

Next, I pass fInt into g to get its results:

val (gInt, gString) = g(fInt)

gInt is the final Int result, so now I glue the strings together to get the final String result:

val logMessage = fString + gString

Now I can print the final Int and String results:

println(s"result: $gInt, log: $logMessage")

This code shows a complete example of the manual solution to this problem:

object Step2Debug 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.")
    }

    // get the output of `f`
    val (fInt, fString) = f(100)

    // plug the Int from `f` as the input to `g`
    val (gInt, gString) = g(fInt)

    // create the total "debug string" by manually merging
    // the strings from f and g
    val debug = fString + " " + gString
    println(s"result: $gInt, debug: $debug")

}

That code prints this output:

result: 600, debug: 
f result: 200. 
g result: 600.

While this approach works for this simple case, imagine what your code will look like when you need to string many more functions together. That would be an awful lot of manually written (and error-prone) code. We can do better.