This is the second part of a series of articles about pattern matching in Scala. In the first part we’ve covered literal pattern matching, matching against different data types and matching on case classes. We’ve decompiled (in Java) the byte code created by Scala just to have a better understanding of the things that happen under the hood. We will continue in the same manner with extractor patterns, xml patterns, matching on arrays and lists.
Extractor patterns
Sometimes we have classes that are not case classes but we want to apply pattern matching on their internal structure. Scala has an elegant solution to this: extractors. All we have to do is to create a companion object and implement the unapply
method. The method expects one argument which represents the object that we want to deconstruct and it returns an option of type TupleN, where N represents the number of values it extracts from the object. Below we have a Division
class with two fields that correspond to the divident
and the divisor
. We would like to extract both values so, the unapply
method returns an Option[(Int, Int)]
.
class Division(val dividend: Int, val divisor: Int) object Division { def unapply(division: Division): Option[(Int, Int)] = Some((division.dividend, division.divisor)) } def eval(division: Division) = division match { case Division(_, 0) => "Division by zero!" case Division(a, b) => a / b } print(eval(new Division(20, 2)))
If we decompile the code we can see how the unapply
method is used (twice?) to extract the values from the object (see the highlighted code).
private final Object eval$1(Division.2 division, ...) { int n; Object object; Option<Tuple2<Object, Object>> option = this.Division$1(Division$module$1).unapply(division); if (!option.isEmpty() && 0 == (n = ((Tuple2)option.get())._2$())) { object = "Division by zero!"; } else { Option<Tuple2<Object, Object>> option2 = this.Division$1(Division$module$1).unapply(division); if (!option2.isEmpty()) { int a = ((Tuple2)option2.get())._1$(); int b = ((Tuple2)option2.get())._2$(); object = BoxesRunTime.boxToInteger((int)(a / b)); } else { throw new MatchError((Object)division); } } return object; }
There are three signatures for the unapply
method which differs in the return type:
def unapply(object: A): Option[B]
used when we want to extract one value,
def unapply(object: A): Option[(B1, ...,Bn)]
used when we want to extract multiple values,
def unapply(object: A): Boolean
used to do a boolean check.
Xml patterns
Scala has great support for XML handling. Remember, Scala was designed in 2000s when Xml was the first choice for data transport. Since version 2.11, Scala Xml is a separate jar maintained by the community (see https://github.com/scala/scala-xml). In Scala you can define Xml literals, like in the code below. And you can apply pattern matching on Xml nodes. The example shows how you can extract the content of all children of book
node.
val book = <book> <author>Steve McConnell</author> <title>Code Complete</title> <description>The best practical guides to programming.</description> </book> def handleBookNode(bookNode: Node) = bookNode match { case <author>{author}</author> => println(s"Author: $author") case <title>{title}</title> => println(s"Title: $title") case <description>{desc}</description> => println(s"Description: $desc") case _ => println("Oops") } for (bookNode <- book \ "_") handleBookNode(bookNode)
I printed below only a small part of the corresponding Java code, the code that handles the author
node. Elem
is one of the main classes in Scala Xml library and we can see that it defines the unapplySeq
method, which is used to extract the information about an Xml node. unapplySeq
returns a Tuple5, on the 2nd position we will find the name of the node and on the 5th position the content. unapplySeq
is the other kind of extractor that can be defined for an object and it is usually used when we want to extract an arbitrary number of parameters from the object. It has more sense for arrays and lists.
Node node = bookNode; Option opt = Elem..MODULE$.unapplySeq(node); if (!opt.isEmpty() && opt.get()._5() != null) { String string = opt.get()._2(); Node author = (Node)(opt.get()._5()).apply(0); if ("author".equals(string)) { Predef..MODULE$.println(...); return; } } ...
Matching on arrays
As previously mentioned, there is a special extractor used when the number of extracted parameters from an object is variable: unapplySeq
. For arrays, it returns an IndexedSeq
from which, for example, the method lengthCompare
is used to check if the array has at least 2 elements (for the first case statement). In the first two case statements _*
means any other elements, including zero elements. For this example, I will not include the corresponding Java code as it is very ugly. I will let this as an exercise for the reader.
val array = Array(1, 2, 3, 4) array match { case Array(x, y, _*) => print(s"first element - $x, second one - $y") case Array(x, _*) => print("only one element") case _ => println("empty!") }
Matching on lists
Finally, we can do interesting stuff with pattern matching on lists as well. There are three case statements in the example below, the first one matches any list with at least two elements (we also see how we can extract the tail of the list). The second case statement matches the lists with one and only one element (Nil
means here that the list has no tail). And lastly, the third case matches empty lists. The second example shows a more useful use case of pattern matching on lists: recursively computing the sum of a list. Decompiling the code revels interesting details about how lists are implemented in Scala, I will also let this as an exercise for the reader.
var list = List(1, 3, 5, 7) list match { case a :: b :: tail => print(s"at least two elements: $a, $b; tail: $tail") case a :: Nil => print(s"the list has exactly one element: $a") case Nil => print("the list is empty") }
def sum(list: List[Int]): Int = { list match { case Nil => 0 case head :: tail => head + sum(tail) } }
Conclusion
Pattern matching is great… there are still things to cover (like guards for example), let’s see, there could be a third part of this series.
Comments are closed, but trackbacks and pingbacks are open.