Functions Are Variables, Too
The goal of this lesson is to show that in a good FP language like Scala, you can use functions as values. In the same way that you create and use String
and Int
values, you can use a function:
val name = "Al" // string value
val weight = 222 // int value
val double = (i: Int) => i * 2 // function value
To support this goal, this lesson shows:
How to define a function as a
val
The “implicit” form of the
val
function syntaxHow to pass a function to another function
Other ways to treat functions as values
Understanding Scala’s val
function syntax is important because you’ll see function signatures over and over in a variety of places, including:
When you define
val
functionsWhen you define function input parameters (i.e., when one function takes another function as an input parameter)
When you’re reading the Scaladoc for almost every method in the Scala collections classes
In the REPL output
You’ll see examples of most of these in this lesson.
Before getting into this lesson, it will help to make sure that I’m formal about how I use certain terminology. For instance, given this expression:
val x = 42
it’s important to be clear about these things:
- Technically,
x
is a variable, a specific type of variable known as an immutable variable. Informally, I prefer to refer tox
as a “value,” as in saying, “x
is an integer value.” I prefer this becausex
is declared as aval
field; it’s bound to theInt
value 42, and that can never change. But to be consistent with (a) other programming resources as well as (b) algebraic terminology, I’ll refer tox
as a variable in this lesson.
Wikipedia states that in algebra, “a variable is an alphabetic character representing a number, called the value of the variable, which is either arbitrary or not fully specified or unknown.” So in this way, referring to
x
as a variable is consistent with algebraic terms.
x
has a type. In this case the type isn’t shown explicitly, but we know that the type is anInt
. I could have also defined it like this:val x: Int = 42
But because programmers and the Scala compiler know that 42
is an Int
, it’s convenient to use the shorter form.
- Variables themselves have values, and in this example the variable
x
has the value42
. (As you can imagine, it might be confusing if I wrote, “The valuex
has the value42
.)”
I’m formalizing these definitions now because as you’re about to see, these terms also apply to creating functions: functions also have variable names, types, and values.
If you haven’t heard of the term “function literal” before, it’s important to know that in this example:
xs.map(x => x * 2)
this part of the code is a function literal:
x => x * 2
It’s just like saying that this is a string literal:
"hello, world"
I mention this because …
In functional programming languages, function literals can be assigned to variable names. In Scala this means:
You can define a function literal and assign it to a
val
field, which creates an immutable variableYou give that variable a name, just like any other
val
fieldA function variable has a value, which is the code in its function body
A function variable has a type — more on this shortly
You can pass a function around to other functions, just like any other
val
You can store a function in a collection, such as a
Map
In general, you use a function variable just like any other variable
In the “Explaining the val
Function Syntax” appendix, I show two different ways to define functions using val
s in Scala. In this lesson I’ll use only the following approach, which shows the “implicit return type” syntax:
val isEven = (i: Int) => i % 2 == 0
In this case “implicit” means that this function doesn’t explicitly state that it returns a Boolean
value; both you and the compiler can infer that by looking at the function body.
Scala also has a
val
function syntax where you explicitly declare the function’s return type, and I show that in the appendix.
I discuss the implicit syntax in detail in the appendix, but Figure [fig:functionImplicitReturnTypeSyntax] shows a quick look at what each of those fields means.
If that syntax looks a little unusual, fear not, I show more examples of it in this lesson and in the appendices.
This function body is a short way of saying that it returns true
if the Int
it is given is an even number, otherwise it returns false
. If you don’t like the way that code reads, it may help to put curly braces around the function body:
val isEven = (i: Int) => { i % 2 == 0 }
Or you can make the if/else condition more obvious:
val isEven = (i: Int) => if (i % 2 == 0) true else false
You can also put curly braces around that function body:
val isEven = (i: Int) => { if (i % 2 == 0) true else false }
Finally, if you prefer a really long form, you can write isEven
like this:
val isEven = (i: Int) => {
if (i % 2 == 0) {
true
} else {
false
}
}
Note: I only show this last version to show an example of a multi-line function body. I don’t recommend writing short functions like this.
If you were going to explain any of these functions to another person, a good explanation would be like this:
“The function
isEven
transforms the inputInt
into aBoolean
value based on its algorithm, which in this case isi % 2 == 0
.”
When you read that sentence, it becomes clear that the Boolean
return value is implied (implicit). I know that when I look at the code I have to pause for a moment before thinking, “Ah, it has a Boolean
return type,” because it takes a moment for my brain to evaluate the function body to determine its return type. Therefore, even though it’s more verbose, I generally prefer to write functions that explicitly specify their return type, because then I don’t have to read the function body to determine the return type.
IMHO, if (a) you have to read a function’s body to determine its return type while (b) what you’re really trying to do is understand some other block of code — such as when you’re debugging a problem — then (c) this forces you to think about low-level details that aren’t important to the problem at hand. That’s just my opinion, but it’s what I have come to believe; I’d rather just glance at the function’s type signature.
Put another way, it’s often easier to write functions that don’t declare their return types, but it’s harder to maintain them.
You can come to understand the implicit val
function syntax by pasting a few functions into the Scala REPL. For instance, when you paste this function into the REPL:
val isEven = (i: Int) => i % 2 == 0
you’ll see that the REPL responds by showing that isEven
is an instance of something called <function1>
:
scala> val isEven = (i: Int) => i % 2 == 0
isEven: Int => Boolean = <function1>
And when you paste a function that takes two input parameters into the REPL:
val sum = (a: Int, b: Int) => a + b
you’ll see that it’s an instance of <function2>
:
scala> val sum = (a: Int, b: Int) => a + b
sum: (Int, Int) => Int = <function2>
When I line up the REPL output for those two examples, like this:
isEven: Int => Boolean = <function1>
sum: (Int, Int) => Int = <function2>
you can begin to see that the general form for the way the REPL displays function variables is this:
variableName: type = value
You can see this more clearly when I highlight the function types and values. This is the REPL output for isEven
:
isEven: Int => Boolean = <function1>
------ -------------- -----------
name type value
and this is the output for the sum
function:
sum: (Int, Int) => Int = <function2>
---- ----------------- -----------
name type value
The type of the isEven
function can be read as, “Transforms an Int
value into a Boolean
value,” and the sum
function can be read as, “Takes two Int
input parameters and transforms them into an Int
.”
Cool FP developers generally don’t say, “a function returns a result.” They say things like, “a function transforms its inputs into an output value.” Or, as it’s stated in the Land of Lisp book, Lisp purists prefer to say that “a function evaluates to a result.” This may seem like a minor point, but I find that using phrases like this helps my brain to think of my code as being a combination of algebraic functions (or equations) — and that’s a good way to think.
In the “Explaining the val
Function Syntax” appendix I write more about this topic, but in short, the output <function1>
indicates that isEven
is an instance of the Function1
trait (meaning that it has one input parameter), and <function2>
means that sum
is an instance of the Function2
trait (meaning that it has two input parameters). The actual “value” of a function is the full body of the function, but rather than show all of that, the REPL uses <function1>
and <function2>
to show that isEven
and sum
are instances of these types.
As I discuss in that appendix, behind the scenes the Scala compiler converts this function:
val sum = (a: Int, b: Int) => a + b
into code that looks a lot like this:
val sum = new Function2[Int, Int, Int] {
def apply(a: Int, b: Int): Int = a + b
}
I don’t want to get too hung up on these details right now, but this is where the Function2
reference comes from. For more information on this topic, see the “Explaining the val
Function Syntax” appendix.
A great thing about functional programming is that you can pass functions around just like other variables, and the most obvious thing this means is that you can pass one function into another. A good way to demonstrate this is with the methods in the Scala collections classes.
For example, given this list of integers (List[Int]
):
val ints = List(1,2,3,4)
and these two functions that take Int
parameters:
val isEven = (i: Int) => i % 2 == 0
val double = (i: Int) => i * 2
you can see that isEven
works great with the List
class filter
method:
scala> ints.filter(isEven)
res0: List[Int] = List(2, 4)
and the double
function works great with the map
method:
scala> ints.map(double)
res1: List[Int] = List(2, 4, 6, 8)
Passing functions into other functions like this is what functional programming is all about.
In the upcoming lessons on Higher-Order Functions I show how to write methods like map
and filter
, but here’s a short discussion of how the process of passing one function into another function (or method) works.
Technically filter
is written as a method that takes a function as an input parameter. Any function it accepts must (a) take an element of the type contained in the collection, and (b) return a Boolean
value. Because in this example filter
is invoked on ints
— which is a List[Int]
— it expects a function that takes an Int
and returns a Boolean
. Because isEven
transforms an Int
to a Boolean
, it works great with filter
for this collection.
The filter
method Scaladoc is shown in Figure [fig:scaladocListFilter]. Notice how it takes a predicate which has the generic type A
as its input parameter, and it returns a List
of the same generic type A
. It’s defined this way because filter
doesn’t transform the list elements, it just filters out the ones you don’t want.
As shown in Figure [fig:scaladocListMap], map
also takes a function that works with generic types. In my example, because ints
is a List[Int]
, you can think of the generic type A
in the image as an Int
. Because map
is intended to let you transform data, the generic type B
can be any type. In my example, double
is a function that takes an Int
and returns an Int
, so it works great with map
.
I explain this in more detail in upcoming lessons, but the important point for this lesson is that you can pass a function variable into another function.
Because functions are variables, you can do all sorts of things with them. For instance, if you define two functions like this:
val double = (i: Int) => i * 2
val triple = (i: Int) => i * 3
you can have fun and store them in a Map
:
val functions = Map(
"2x" -> double,
"3x" -> triple
)
If you put that code into the REPL, you’ll have two functions stored as values inside a Map
.
Now that they’re in there, you can pass the Map
around as desired, and then later on get references to the functions using the usual Map
approach, i.e., by supplying their key values. For example, this is how you get a reference to the double
function that’s stored in the Map
:
scala> val dub = functions("2x")
d: Int => Int = <function1>
This is just like getting a String
or an Int
or any other reference out of a Map
— you specify the key that corresponds to the value.
Now that you have a reference to the original double
function, you can invoke it:
scala> dub(2)
res0: Int = 4
You can do the same things with the other function I put in the Map
:
scala> val trip = functions("3x")
t: Int => Int = <function1>
scala> trip(2)
res1: Int = 6
These examples show how to create functions as variables, store them in a Map
, get them out of the Map
, and then use them.
Besides showing how to put function variables into Map
s, a key point of this example is: in Scala you can use a function variable just like a String
variable or an Int
variable. The sooner you begin treating functions as variables in your own code, the further you’ll be down the path of becoming a great functional programmer.
Given what I’ve shown so far, this request may be a bit of an advanced exercise, but … here’s that Map
example again:
val functions = Map(
"2x" -> double,
"3x" -> triple
)
Given that Map
, sketch its data type here:
As an example of what I’m looking for, this Map
:
val m = Map("age" -> 42)
has a data type of:
Map[String, Int]
That’s what I’m looking for in this exercise: the type of the Map
named functions
.
If you pasted the Map
code into the REPL, you saw its output:
Map[String, Int => Int] = Map(2x -> <function1>, 3x -> <function1>)
The first part of that output shows the Map
’s data type:
Map[String, Int => Int]
The data type for the Map
’s key is String
, and the type for its value is shown as Int => Int
. That’s how you write the type for a function that transforms a single Int
input parameter to a resulting Int
value. As you know from the previous discussion, this means that it’s an instance of the Function1
trait.
As a second example, if the Map
was holding a function that took two Int
’s as input parameters and returns an Int
— such as the earlier sum
function — its type would be shown like this:
Map[(Int, Int) => Int]
That would be a Function2
instance, because it takes two input parameters.
To help you get comfortable with the “implicit return type” version of the val
function syntax, here are the functions I showed in this lesson:
val isEven = (i: Int) => i % 2 == 0
val sum = (a: Int, b: Int) => a + b
val double = (i: Int) => i * 2
val triple = (i: Int) => i * 3
And here are a few more functions that show different input parameter types:
val strlen = (s: String) => s.length
val concat = (a: String, b: String) => a + b
case class Person(firstName: String, lastName: String)
val fullName = (p: Person) => s"${p.firstName} ${p.lastName}"
Here’s a summary of what I showed in this lesson:
Function literals can be assigned to
val
fields to create function variablesTo be consistent with algebra and other FP resources, I refer to these fields are variables rather than values
Examples of the
val
function syntaxA function is an instance of a
FunctionN
trait, such asFunction1
orFunction2
What various function type signatures look like in the REPL
How to pass a function into another function
How to treat a function as a variable by putting it in a
Map
That, in general, you can use a function variable just like any other variable
In regards to val
function signatures, understanding them is important because you’ll see them in many places, including function literals, the Scaladoc, REPL output, and other developer’s code. You’ll also need to know this syntax so you can write your own functions that take other functions as input parameters.
The next lesson shows that you can use def
methods just like val
functions. That’s important because most developers prefer to use the def
method syntax to define their algorithms.
- Scala’s Function1 trait