Mostly when we, as programmers, think of pattern matching we think of regular expressions. But in the context of functional programming, this terminology takes on a new meaning. Instead of regular expression matching, we’re going to be looking at matching objects against other objects.
Using pattern matching, you can extract from objects, match on members of objects, and verify that objects are of specific types—all within a statement. Pattern matching allows for fewer lines of variable assignment and more lines of understandable code. With pattern matching, you can match on members of an object, which allows you to write more concise logic for when a specific segment of code should be executed.
Now that the code has started shaping up, our boss has asked us to create a new function that will create a new Customer
. The requirements are as follows:
name
cannot be blank.
state
cannot be blank.
domain
cannot be blank.
enabled
must be true to start with.
contract
will be created based on today’s date.
contacts
should be created as a blank list for now.
Our basic method, as shown in Example 8-1, uses a large if
structure to return null
in the event that an invalid value is passed in. We’re currently printing to the console, but we should also log the message being sent.
if
(
name
.
isEmpty
)
{
println
(
"Name cannot be blank"
)
null
}
else
if
(
state
.
isEmpty
)
{
println
(
"State cannot be blank"
)
null
}
else
if
(
domain
.
isEmpty
)
{
println
(
"Domain cannot be blank"
)
null
}
else
{
new
Customer
(
0
,
name
,
state
,
domain
,
true
,
new
Contract
(
Calendar
.
getInstance
,
true
),
List
()
)
}
This is a huge if
structure that we do not want to maintain. Think back to Chapter 1, in which we were creating extractors from our Customer
objects by using an if
statement. We’re almost doing the same thing here, using a giant if
structure to determine whether certain fields are blank. By the end of our examples, we’re going to see how this will become a much more manageable check.
For now, let’s perform a simple refactor by using a very basic pattern match in Example 8-2. Using a pattern match is fairly straightforward in this instance: we’re going to match each of our elements against a blank string, ""
.
The variable before the match
keyword is what we’ll pattern-match against. Inside the match
statement are all of the patterns that we’re going to test against, each defined with the case
keyword. Right now, we just have ""
, which indicates a blank string, and the underscore _
, which indicates anything.
def
createCustomer
(
name
:
String
,
state
:
String
,
domain
:
String
)
:
Customer
=
{
name
match
{
case
""
=>
{
println
(
"Name cannot be blank"
)
null
}
case
_
=>
state
match
{
case
""
=>
{
println
(
"State cannot be blank"
)
null
}
case
_
=>
domain
match
{
case
""
=>
{
println
(
"Domain cannot be blank"
)
null
}
case
_
=>
new
Customer
(
0
,
name
,
state
,
domain
,
true
,
new
Contract
(
Calendar
.
getInstance
,
true
),
List
()
)
}
}
}
}
Remember, we’re transitioning to a better pattern match, so our first step has been to re-create the if/else
structure, but in a pattern-match style. At first it seems like this has created an even larger mess, but don’t worry: we’re going to reduce the complexity quite a bit in the next sections.
Let’s modify the pattern match that we created in createCustomer
to be only one level deep. We can do this by creating a tuple (a group of elements) that we can then match against. Let’s see this refactor in Example 8-3.
We are now defining a tuple (name, state, domain)
against which we’re going to match. What is so different here is that now we can match against each part of the tuple. We do this with case ("", _, _)
which lets us say that this pattern should be a tuple with a blank string as the first value, and we don’t care what the other two are.
def
createCustomer
(
name
:
String
,
state
:
String
,
domain
:
String
)
:
Customer
=
{
(
name
,
state
,
domain
)
match
{
case
(
""
,
_
,
_
)
=>
{
println
(
"Name cannot be blank"
)
null
}
case
(
_
,
""
,
_
)
=>
{
println
(
"State cannot be blank"
)
null
}
case
(
_
,
_
,
""
)
=>
{
println
(
"Domain cannot be blank"
)
null
}
case
_
=>
new
Customer
(
0
,
name
,
state
,
domain
,
true
,
new
Contract
(
Calendar
.
getInstance
,
true
),
List
()
)
}
}
Now that we have a way to convert if
statements into pattern matches, let’s see if we can convert another large if/else
structure in our code base. Let’s look at the original setContractForCustomerList
method, shown in Example 8-4, which handles blank initialIds
and ids
parameters with a large if
statement. Inside the else
, we find the original Customer
by id
; if the customer is defined, we will execute our cls
to update the Customer
, putting it into a list. We then merge the list containing our updated Customer
with the return of the recursive call.
def
updateCustomerByIdList
(
initialIds
:
List
[
Customer
],
ids
:
List
[
Integer
],
cls
:
Customer
=>
Customer
)
:
List
[
Customer
]
=
{
if
(
ids
.
size
<=
0
)
{
initialIds
}
else
if
(
initialIds
.
size
<=
0
)
{
initialIds
}
else
{
val
precust
=
initialIds
.
find
(
cust
=>
cust
.
customer_id
==
ids
(
0
))
val
cust
=
if
(
precust
.
isEmpty
)
{
List
()
}
else
{
List
(
cls
(
precust
.
get
))
}
cust
:::
updateCustomerByIdList
(
initialIds
.
filter
(
cust
=>
cust
.
customer_id
==
ids
(
0
)),
ids
.
tail
,
cls
)
}
}
We know how to handle this via pattern matching, so let’s wrap those two variables into a tuple and match against a blank list. Much like our blank string ""
, we can imitate the blank list with List()
, as shown in Example 8-5.
def
updateCustomerByIdList
(
initialIds
:
List
[
Customer
],
ids
:
List
[
Integer
],
cls
:
Customer
=>
Customer
)
:
List
[
Customer
]
=
{
(
initialIds
,
ids
)
match
{
case
(
List
(),
_
)
=>
initialIds
case
(
_
,
List
())
=>
initialIds
case
_
=>
{
val
precust
=
initialIds
.
find
(
cust
=>
cust
.
customer_id
==
ids
(
0
))
val
cust
=
if
(
precust
.
isEmpty
)
{
List
()
}
else
{
List
(
cls
(
precust
.
get
))
}
cust
:::
updateCustomerByIdList
(
initialIds
.
filter
(
cust
=>
cust
.
customer_id
==
ids
(
0
)),
ids
.
drop
(
1
),
cls
)
}
}
}
But can we reduce the complexity of this method even further? Yes—by introducing extractors, specifically list extractors.
As their name implies, you can use extractors to pattern-match based on the object and extract members from the object itself. We’ll see how to extract elements out of objects in the next section, but right now let’s look at extracting from a list.
As you might recall, lists have a head and a tail, and we should be able to move through our list one item at a time by looking at the head and passing the tail to look at later. So let’s check out the list extraction in Example 8-6 to see how we can move through our ids
variable.
The ::
operator, when used in a case
statement, tells Scala that a list is expected and that the list should be decomposed into its head element (to the left of the operator) and its tail element (to the right of the operator). The variables into which the items are extracted exist only during the specific pattern execution.
The case (_, id
pattern will extract the head of the ::
tailIds)ids
variable into a new variable called id
and the tail of the ids
into a new variable called tailIds
.
def
updateCustomerByIdList
(
initialIds
:
List
[
Customer
],
ids
:
List
[
Integer
],
cls
:
Customer
=>
Customer
)
:
List
[
Customer
]
=
{
(
initialIds
,
ids
)
match
{
case
(
List
(),
_
)
=>
initialIds
case
(
_
,
List
())
=>
initialIds
case
(
_
,
id
::
tailIds
)
=>
{
val
precust
=
initialIds
.
find
(
cust
=>
cust
.
customer_id
==
id
)
val
cust
=
if
(
precust
.
isEmpty
)
{
List
()
}
else
{
List
(
cls
(
precust
.
get
))
}
cust
:::
updateCustomerByIdList
(
initialIds
.
filter
(
cust
=>
cust
.
customer_id
==
id
),
tailIds
,
cls
)
}
}
}
We’re going to convert the find
return into a list and then pattern-match against it. There are two possibilities here: either we will have a blank list, or we want the head element from the list itself. Let’s look at the code in Example 8-7, in which we are doing this match.
The find
return is being converted into a list for us to match against. We then perform the match on that and determine whether the list is blank or has elements (in which case we take the first one).
def
updateCustomerByIdList
(
initialIds
:
List
[
Customer
],
ids
:
List
[
Integer
],
cls
:
Customer
=>
Customer
)
:
List
[
Customer
]
=
{
(
initialIds
,
ids
)
match
{
case
(
List
(),
_
)
=>
initialIds
case
(
_
,
List
())
=>
initialIds
case
(
_
,
id
::
tailIds
)
=>
{
val
precust
=
initialIds
.
find
(
cust
=>
cust
.
customer_id
==
id
).
toList
precust
match
{
case
List
()
=>
updateCustomerByIdList
(
initialIds
,
tailIds
,
cls
)
case
cust
::
custs
=>
updateCustomerByIdList
(
initialIds
.
filter
(
cust
=>
cust
.
customer_id
==
id
),
tailIds
,
cls
)
}
}
}
}
So, why are we converting the return of find
to a list? Well, the find
method returns an Option
, which is a generic interface that has two implementing classes: Some
or None
. As you might have guessed, the Some
class will actually contain the object, whereas the None
object contains nothing. We can convert the Option
object to a List
, which we can then pattern-match against.
However, we can actually pattern-match against the Option
interface and reduce the need to convert it to a list. We’ll get rid of our precust
variable as well as the toList
conversion. Instead, we’re just going to send the find
result directly to our pattern match.
We will create two case
statements: one to match on the None
object, and the other to match on Some
. Notice in Example 8-8 that when we match on Some
, we can use the syntax Some(cust)
, which allows us to extract the member of Some
into our own variable, cust
.
def
updateCustomerByIdList
(
initialIds
:
List
[
Customer
],
ids
:
List
[
Integer
],
cls
:
Customer
=>
Customer
)
:
List
[
Customer
]
=
{
(
initialIds
,
ids
)
match
{
case
(
List
(),
_
)
=>
initialIds
case
(
_
,
List
())
=>
initialIds
case
(
_
,
id
::
tailIds
)
=>
{
initialIds
.
find
(
cust
=>
cust
.
customer_id
==
id
)
match
{
case
None
=>
updateCustomerByIdList
(
initialIds
,
tailIds
,
cls
)
case
Some
(
cust
)
=>
updateCustomerByIdList
(
initialIds
.
filter
(
cust
=>
cust
.
customer_id
==
id
),
tailIds
,
cls
)
}
}
}
}
What is that Some
class, and how is it that we are able to extract members of the objects into variables? The Some
class is actually a case class
, and as we’ll see in the next section, we can actually match and extract members of case class
es.
Pattern matching includes the idea of matching on objects and extracting the fields from an object. As we’ve already seen in some of our examples, the Option pattern allows us to indicate either None
or Some
. With Some
, we can encapsulate and get some value without having to write an if
structure like the one shown in Example 8-9.
var
foo
:
Option
[
String
]
=
Some
(
"Bar"
)
if
(
obj
.
isDefined
)
{
obj
.
get
}
else
{
""
/* Not defined */
}
Instead, we can write a pattern match against Option
and make it much more readable, as shown in Example 8-10.
var
foo
:
Option
[
String
]
=
Some
(
"Bar"
)
obj
match
{
case
None
=>
""
case
Some
(
o
)
=>
o
}
We no longer have to write any if
statements to compare types or isDefined
calls. Instead, the pattern match handles the object comparison for us. We can do even more matches by looking inside the object, much as we did with the Option
example. Let’s say we have a Some
object with the contents Bar
. We can use the case
syntax of case Some("Bar")
to match on the value inside the case
object. Let’s see this in Example 8-11.
var
foo
:
Option
[
String
]
=
Some
(
"Bar"
)
obj
match
{
case
None
=>
""
case
Some
(
"Bar"
)
=>
"Foo"
case
Some
(
o
)
=>
o
}
What is really interesting about this Option pattern is that we can use it in our createCustomer
method. Remember the function in Example 8-3? Well, we can actually improve it by returning a None
object (which does extend Option
) on error, and returning Some
if successful. Let’s see this in Example 8-12.
def
createCustomer
(
name
:
String
,
state
:
String
,
domain
:
String
)
:
Option
[
Customer
]
=
{
(
name
,
state
,
domain
)
match
{
case
(
""
,
_
,
_
)
=>
{
println
(
"Name cannot be blank"
)
None
}
case
(
_
,
""
,
_
)
=>
{
println
(
"State cannot be blank"
)
None
}
case
(
_
,
_
,
""
)
=>
{
println
(
"Domain cannot be blank"
)
None
}
case
_
=>
new
Some
(
new
Customer
(
0
,
name
,
state
,
domain
,
true
,
new
Contract
(
Calendar
.
getInstance
,
true
),
List
()
)
)
}
}
Here is the really interesting thing: we can actually make this more functional and encapsulate the print
statement (logging) and return None
because there is no reason to repeat ourselves. We can extract this into an error
function that only needs to exist inside the createCustomer
function. See the refactored code in Example 8-13.
def
createCustomer
(
name
:
String
,
state
:
String
,
domain
:
String
)
:
Option
[
Customer
]
=
{
def
error
(
message
:
String
)
:
Option
[
Customer
]
=
{
println
(
message
)
None
}
(
name
,
state
,
domain
)
match
{
case
(
""
,
_
,
_
)
=>
error
(
"Name cannot be blank"
)
case
(
_
,
""
,
_
)
=>
error
(
"State cannot be blank"
)
case
(
_
,
_
,
""
)
=>
error
(
"Domain cannot be blank"
)
case
_
=>
new
Some
(
new
Customer
(
0
,
name
,
state
,
domain
,
true
,
new
Contract
(
Calendar
.
getInstance
,
true
),
List
()
)
)
}
}
There’s another scenario in which converting from an if
structure to a pattern match would actually increase readability. Let’s look at the original countEnabledCustomersWithNoEnabledContacts
method shown in Example 8-14.
def
countEnabledCustomersWithNoEnabledContacts
(
customers
:
List
[
Customer
],
sum
:
Integer
)
:
Integer
=
{
if
(
customers
.
isEmpty
)
{
sum
}
else
{
val
addition
=
if
(
customers
.
head
.
enabled
&&
customers
.
head
.
contacts
.
exists
({
contact
=>
contact
.
enabled
}))
{
1
}
else
{
0
}
countEnabledCustomersWithNoEnabledContacts
(
customers
.
tail
,
addition
+
sum
)
}
}
Now that we know how to extract from lists, we will try to rewrite this function. The first thing to do is define our Customer
object as a case class
, as shown in Example 8-15, by simply adding the case
keyword to the class
keyword.
case
class
Customer
(
val
customer_id
:
Integer
,
val
name
:
String
,
val
state
:
String
,
val
domain
:
String
,
val
enabled
:
Boolean
,
val
contract
:
Contract
,
val
contacts
:
List
[
Contact
])
{
}
Now let’s look at Example 8-16. Notice that we are going to handle the empty list first, then use the same type of syntax with the Some()
object, except here we extract only the enabled
and contacts
of the Customer
and ignore the rest.
For the enabled
field, we want to match only if true
is set for that field. We also want to pull out the Contact
list into the cont
variable.
Next, we have an if
statement before our =>
, which is called a guard. It allows us to match a pattern but only if a specific condition occurs. Finally, we call back into our function with the tail of our list and our sum + 1.
def
countEnabledCustomersWithNoEnabledContacts
(
customers
:
List
[
Customer
],
sum
:
Integer
)
:
Integer
=
{
customers
match
{
case
List
()
=>
sum
case
Customer
(
_
,
_
,
_
,
_
,
true
,
_
,
cont
)
::
custs
if
cont
.
exists
({
contact
=>
contact
.
enabled
})
=>
countEnabledCustomersWithNoEnabledContacts
(
custs
,
sum
+
1
)
case
cust
::
custs
=>
countEnabledCustomersWithNoEnabledContacts
(
custs
,
sum
)
}
}
Now we can make this more efficient fairly easily: we can add a pattern to skip over our Contact
list for the customer if it is blank, as shown in Example 8-17.
def
countEnabledCustomersWithNoEnabledContacts
(
customers
:
List
[
Customer
],
sum
:
Integer
)
:
Integer
=
{
customers
match
{
case
List
()
=>
sum
case
Customer
(
_
,
_
,
_
,
_
,
true
,
_
,
List
())
::
custs
=>
countEnabledCustomersWithNoEnabledContacts
(
custs
,
sum
)
case
Customer
(
_
,
_
,
_
,
_
,
true
,
_
,
cont
)
::
custs
if
cont
.
exists
({
contact
=>
contact
.
enabled
})
=>
countEnabledCustomersWithNoEnabledContacts
(
custs
,
sum
+
1
)
case
cust
::
custs
=>
countEnabledCustomersWithNoEnabledContacts
(
custs
,
sum
)
}
}
Throughout this chapter we’ve done quite a bit with pattern matching; we’ve actually converted our if
structures into pattern matches. This has enabled us to perform simpler recursive loops over lists by using extractions from the lists. We have also been able to simplify our cases by matching on members inside the objects to reduce the amount of logic that we need to write.
We’ve also learned about the Option pattern, which allows us to get away from null
objects by handling cases through pattern matching and either extracting the Some
or handling a None
case, as appropriate.