Starting To Glue Pure 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.
Update: All of my new videos are now on
LearnScala.dev