Functions: Multiple Input Parameter Groups (Scala 3 Video)
Note: The following text is an abridged version of the lesson in my book, Learn Functional Programming The Fast Way!
Functions can have multiple input parameter groups, like this:
def sum(a: Int)(b: Int): Int = a + b
That function can be called like this:
val x = sum(1)(2)
Compare that to a “normal” function, which looks like this:
def sum(a: Int, b: Int): Int = a + b
At first I couldn’t understand why this was an important feature in Scala, but the reality is that it’s very important. When combined with Call By-Name parameters (CBNs), it’s one of the features that lets us write our own control structures, and create our own Domain-Specific Languages (DSLs).
To understand this, we need to look at CBNs.
Call by-name parameters in Scala
Most input parameters are “Call By-Value” (CBV) parameters, meaning that when a variable has a value --- such as x
having the value 42
--- when that variable is passed into a function, the function receives its value, 42
.
Conversely, “by-name” parameters are quite different than by-value parameters. Rob Norris makes the observation that you can think about the two types of parameters like this:
- A by-value parameter is like receiving a
val
field; its body is evaluated once, right before the parameter is bound to the function. - A by-name parameter is like receiving a
def
method; its body is evaluated later, whenever it’s used inside the function.
Example 1: Defining Call By-Name parameters in functions
def exec(blockOfCode: => Unit): Unit =
blockOfCode
In this example, blockOfCode
is a CBN parameter. Notice that the difference between a “normal” CBV input parameter and a CBN parameter is the use of the =>
transformation symbol in the parameter’s definition:
def f(param: Int) ... // CBV parameter
def f(param: => Int) ... // CBN parameter
--
Because =>
always means transform in Scala, you get the idea that param
is something that is transformed into an Int
in this example.
Now, going back to exec
:
def exec(blockOfCode: => Unit): Unit =
blockOfCode
You can read this input parameter as, “blockOfCode
is some piece of code that returns Unit
when it runs.” Or you can say, “blockOfCode
is transformed into the Unit
type after it runs.” Either of those statements is accurate.
This means that you can pass exec
any code block that has the Unit
return type, including a one-line block of code:
exec(println(1))
----------
or a multiline block of code enclosed in curly braces:
exec {
val i = 42
println(s"Secret of the universe = $i")
}
Because println
has the Unit
return type, both of those work.
Example 2: Executing Call By-Name parameters in your function
Another great feature of CBN parameters is that you can use them anywhere inside your function. You can even call them multiple times inside your function, if your algorithm calls for that.
To demonstrate the anywhere aspect, here’s another example, where I call codeBlock
in the second line of the function. Also notice that I define codeBlock
to yield an Int
instead of Unit
:
def exec(codeBlock: => Int): Unit =
println("about to run the code")
val result = codeBlock
println(s"ran the code, result = $result")
The REPL shows how exec
works with a little block of code like 2 + 2
:
scala> exec(2 + 2)
about to run the code
ran the code, result = 4
Because exec
takes any block of code that yields an Int
, you can also pass functions into it, like these:
def sum(a: Int, b: Int) = a + b
exec(sum(2, 3))
def multiply(a: Int, b: Int) = a * b
exec(multiply(2, 3))
That last example looks like this in the REPL:
scala> exec(multiply(2, 3))
about to run the code
ran the code, result = 6
Example 3: CBNs and multiple parameter groups
Before we get into the next example, a piece of knowledge you need to know is that Scala functions can be defined to have multiple input parameter groups. One of the simplest possible examples looks like this:
def sum(a: Int)(b: Int): Int = a + b
When a function is defined with two parameter groups like that, you just pass your parameters into it in two separate sets of parentheses when you call it:
scala> sum(1)(1)
val res0: Int = 2
This feature enables us to do some cool things, such as to write our own control structures and domain-specific languages (DSLs), especially when combined with CBNs.
To demonstrate this, imagine that you want to create your own control structure named doubleIf
that can be used like this:
doubleIf(age > 18)(numAccidents == 0) {
println("Discount!") // some block of code here
}
As shown, you pass it two test conditions in the first two parameter groups:
doubleIf(age > 18)(numAccidents == 0) ...
-------- -----------------
and if both of those are true
, doubleIf
executes your block of code, which is between the curly braces, as the third parameter group.
If doubleIf
sounds difficult to write — fear not! — it’s relatively simple, as these things go. I just mentioned that doubleIf
is a function that has three parameter groups, so you can start sketching it like this:
def doubleIf()()()
The first two parameter groups are test conditions, which means that they are both code blocks — CBN parameters — that yield a Boolean
value:
def doubleIf(test1: => Boolean)(test2: => Boolean)(???)
Then the third parameter group is the block of code the caller wants to run if the two test conditions are true
. In my case I’ll require that this returns Unit
, to keep things simple:
def doubleIf(test1: => Boolean)(test2: => Boolean)(codeBlock: => Unit)
And finally, the body of the function says, “If both test conditions are true
, run the block of code”:
// test1 and test2 are shortened to t1 and t2 to fit the book’s page width
def doubleIf(t1: => Boolean)(t2: => Boolean)(codeBlock: => Unit): Unit =
if t1 && t2 then codeBlock
Now if you put this following code in the REPL, you’ll see that the "Discount!"
text is printed:
val age = 20
val numAccidents = 0
doubleIf(age > 18)(numAccidents == 0) {
println("Discount!")
}
But if both of those conditions are not true
, you’ll see that "Discount!"
is not printed.
As I hope this example shows, CBNs combined with multiple parameter groups give you the power to create your own control structures, and eventually your own DSLs.
Note: Better names for by-name parameters?
According to Wikipedia, these terms date back to a language named ALGOL 60 (yes, the year 1960). I don’t mean any disrespect to that language, but for me, the term “by-name” isn’t very helpful (though I’m sure it had a solid meaning back then).
These days I find that the following terms are more accurate and meaningful:
- Evaluate on access
- Evaluate on use
- Evaluate when accessed
- Evaluate when referenced
With any of those phrases you can replace “Evaluate” with words like “Execute” or “Run,” if you prefer.
Key points
As a quick recap, these are some key points about CBNs:
- You define them with the
=>
symbol - You must specify their return type
- They’re not executed until you call them in your function
- They can be called anywhere in your function
- They can be multiple times in your function
- Combined with multiple parameter groups, they let us write our own control structures and DSLs
Update: All of my new videos are now on
LearnScala.dev