This article is for Scala programmers who are curious about the next features in Scala 3. In this, we are discussing particularly Match Type.
Pattern Matching is one of the most powerful construct tools in Scala. One can say it is a powerful form of switch statements of Java or C++. We will get to know what are the advancement done in Scala 3 from Scala 2.
The Problem Statement in Scala 3:
Let’s assume you are working on a library for standard data types(e. g. Int, String, Lists), and you want to write a piece of code that extracts the last constituent part of a bigger value:
- assuming Big-Int is made of digits, the last part is the last digit
- the last part of a String is a Char
- the last part of a list is the element on its last position
def lastDigitOf(number: BigInt): Int = (number % 10).toInt def lastCharOf(string: String): Char = if string.isEmpty then throw new NoSuchElementException else string.charAt(string.length - 1) def lastElemOf[T](list: List[T]): T = if list.isEmpty then throw new NoSuchElementException else list.last
Now one thing that is you notice that all these functions are doing a similar task to extracting the last element. So why not reduce this API into one large single API which does this for us. Besides that you would want to think about the future, perhaps extending this same logic to completely unrelated types as well.
Can you Unify these methods in Scala 2?
No, Scala 2 cannot take these methods which will have a single signature but will execute different pieces of code. But the good news is, it is possible in Scala 3
In Scala 3, we can define a type member which can take different forms; i.e. reduce to different concrete types; depending on the type argument we’re passing:
type ConstituentPartOf[T] = T match case BigInt => Int case String => Char case List[t] => t
This is called a match type. Think of it like a pattern match done on types, by the compiler. The following expressions would all be valid:
val aNumber: ConstituentPartOf[BigInt] = 2 val aCharacter: ConstituentPartOf[String] = 'a' val anElement: ConstituentPartOf[List[String]] = "Scala"
Now let’s see how match types can help solve our first-world problem. Because all the previous methods have the meaning of “extract the last part of a bigger thing”, we can use the match type we’ve just created to write the following all-powerful API:
def lastComponentOf[T](thing: T): ConstituentPartOf[T] = thing match case b: BigInt => (b % 10).toInt case s: String => if (s.isEmpty) throw new NoSuchElementException else s.charAt(s.length - 1) case l: List[_] => if (l.isEmpty) throw new NoSuchElementException else l.last
This method, in theory, can work with any type for which the relationship between T and ConstituentPartOf[T] can be successfully established by the compiler. So if we could implement this method, we could simply use it on all types we care about in the same way:
val lastDigit = lastComponentOf(BigInt(53728573)) // 3 val lastChar = lastComponentOf("Scala") // 'a' val lastElement = lastComponentOf((1 to 10).toList) // 10
Why use Match Type?
One of the question that comes to our mind is why don’t we go with regular inheritance based object oriented programming. Because its easy to code and will do the same functionality.
Because if you write code against an interface, e.g.
you lose the type safety of your API, because the real instance is returned at runtime. At the same time, the returned types must all be related, since they must all derive from a mother-trait.
Also the lastComponentOf method allows the compiler to be flexible in terms of the returned type, depending on the type definition
We learned about match types, which are able to solve a very flexible API unification problem. I’m sure some of you will probably dispute the seriousness of the problem to begin with, but it’s a powerful tool to have in your type-level arsenal, when you’re defining your own APIs or libraries.