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:
- 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). - The second parameter is
fResult
, which is the output off
.f
’s signature tells us thatfResult
’s type is(Int, String)
(i.e., aTuple2[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 asTuple2[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:
- Apply the function you’re given (
g
) to theInt
you’re given (fInt
). This creates an(Int, String)
((gInt, gString)
in this example). - Append the new string (
gString
) to the string you were given (fString
). - Return the new
Int
(gInt
) and the mergedString
(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
, andh
- 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