The Neophyte's Guide to Scala Part 3: Patterns Everywhere
In the first two parts of this series, I spent quite some time explaining what’s actually happening when you destructure an instance of a case class in a pattern, and how to write your own extractors, allowing you to destructure any types of objects in any way you desire.
Now it is time to get an overview of where patterns can actually be used in your Scala code, because so far you have only seen one of the different possible ways to make use of patterns. Here we go!
Pattern matching expressions
One place in which patterns can appear is inside of a pattern matching expression. This way of using patterns should be very familiar to you after attending the Scala course at Coursera and following along in this series. You have some expression e
, followed by the match
keyword and a block, which can contain any number of cases. A case, in turn, consists of the case
keyword followed by a pattern and, optionally, a guard clause on the left side, plus a block on the right side, to be executed if this pattern matches.
Here is a simple example, making use of patterns and, in one of the cases, a guard clause:
case class Player(name: String, score: Int)
def printMessage(player: Player) = player match {
case Player(_, score) if score > 100000 => println("Get a job, dude!")
case Player(name, _) => println("Hey " + name + ", nice to see you again!")
}
The printMessage
method has a return type of Unit
, its sole purpose is to perform a side effect, namely printing a message. It is important to remember that you don’t have to use pattern matching as you would use switch statements in languages like Java. What we are using here is called a pattern matching expression for a reason. Their return value is what is returned by the block belonging to the first matched pattern.
Usually, it’s a good idea to take advantage of this, as it allows you to decouple two things that do not really belong together, making it easier to test your code, too. We could rewrite the example above as follows:
def message(player: Player) = player match {
case Player(_, score) if score > 100000 => "Get a job, dude!"
case Player(name, _) => "Hey " + name + ", nice to see you again!"
}
def printMessage(player: Player) = println(message(player))
Now, we have a separate message
method whose return type is String
. This is essentialy a pure function, returning the result of a pattern matching expression. You could also store the result of such a pattern matching expression as a value or assign it to a variable, of course.
Patterns in value definitions
Another place in which a pattern can occur in Scala is in the left side of a value definition (and in a variable definition, for that matter, but we want write our Scala code in a functional style, so you won’t see a lot of usage of variables in this series). Let’s assume we have a method that returns our current player. We will use a dummy implementation that always returns the same player:
def currentPlayer(): Player = Player("Daniel", 3500)
Your usual value definition looks like this:
val player = currentPlayer()
doSomethingWithTheName(player.name)
If you know Python, you are probably familiar with a feature called sequence unpacking. The fact that you can use any pattern in the left side of a value definition or variable definition lets you write your Scala code in a similar style. We could change our above code and destructure the given current player while assigning it to the left side:
val Player(name, _) = currentPlayer()
doSomethingWithTheName(name)
You can do this with any pattern, but generally, it is a good idea to make sure that your pattern always matches. Otherwise, you will be the witness of an exception at runtime. For instance, the following code is problematic. scores
is a method returning a list of scores. In our code below, this method simply returns an empty list to illustrate the problem.
def scores: List[Int] = List()
val best :: rest = scores
println("The score of our champion is " + best)
Oops, we’ve got a MatchError
. It seems like our game is not that successful after all, having no scores whatsoever.
A safe and very handy way of using patterns in this way is for destructuring case classes whose type you know at compile time. Also, when working with tuples, this makes your code a lot more readable. Let’s say we have a function that returns the name of a player and their score as a tuple, not using the Player
class we have used so far:
def gameResult(): (String, Int) = ("Daniel", 3500)
Accessing the fields of a tuple always feels very awkward:
val result = gameResult()
println(result._1 + ": " + result._2)
It’s safe to destructure our tuple in the value definition, as we know we are dealing with a Tuple2
:
val (name, score) = gameResult()
println(name + ": " + score)
This is much more readable, isn’t it?
Patterns in for comprehensions
Patterns also have a very valuable place in for comprehensions. For one, a for comprehension can also contain value definitions. And everything you learnt about the usage of patterns in the left side of value definitions holds true for value definitions in for comprehensions. So if we have a collection of results and want to determine the hall of fame, which in our game is simply a collection of the names of players that have trespassed a certain score threshold, we could do that in a very readable way with a for comprehension:
def gameResults(): Seq[(String, Int)] =
("Daniel", 3500) :: ("Melissa", 13000) :: ("John", 7000) :: Nil
def hallOfFame = for {
result <- gameResults()
(name, score) = result
if (score > 5000)
} yield name
The result is List("Melissa", "John")
, since the first player does not meet the condition of the guard clause.
This can be written even more concisely, because in for comprehensions, the left side of a generator is also a pattern. So, instead of first assigning each game result to result
, we can directly destructure the result in the left side of the generator:
def hallOfFame = for {
(name, score) <- gameResults()
if (score > 5000)
} yield name
In this example, the pattern (name, score)
always matches, so if it were not for the guard clause, if (score > 5000)
, the for comprehension would be equivalent to simply mapping from the tuples to the player names, without filtering anything.
It is important to know that patterns in the left side of generators can already be used for filtering purposes – if a pattern on the left side of a generator does not match, the respective element is filtered out.
To illustrate, let’s say we have a sequence of lists, and we want to return the sizes of all non-empty lists. This means we have to filter out all empty lists and then return the sizes of the ones remaining. Here is one solution:
val lists = List(1, 2, 3) :: List.empty :: List(5, 3) :: Nil
for {
list @ head :: _ <- lists
} yield list.size
The pattern on the left side of the generator does not match for empty lists. This will not throw a MatchError
, but result in any empty list being removed. Hence, we get back List(3, 2)
.
Patterns and for comprehensions are a very natural and powerful combination, and if you work with Scala for some time, you will see that you’ll be using them a lot.
Anonymous functions
Finally, patterns can be used for defining anonymous functions. If you have ever used a catch
block in order to deal with an exception in Scala, then you have made used of this feature. Pattern matching anonymous functions is a subject that warrants its own blog post, because there is a lot to be said about it. Hence, I will refrain from delving into this usage of patterns in this article, instead leaving you with the promise of dealing with it in the next part of the series.
Update: Fixed a mistake in the expected result of the hallOfFame
for comprehension. Thanks to Rajiv for pointing it out.
Posted by Daniel Westheide