Functional programming gained a big momentum in the IT field; many things come and go, but FP is not one of them. It is much more expressive than OOP
I started dig into it a few years ago, during LambaConf in Bologna and the more insight i get, the more i love FP. In June i went to this workshop and, the teacher went deep on algebraic data types and Pattern matching on them. I also finally understood what a Monad is 🎉, but this is another story 😊. I used to think about pattern matching as an interesting way to destructure lists. Now i know that it is a founding block of FP software design, and an effective way to address the need of decoupling domain objects data and behaviors.
Let’s see an example!
Let’s say that we want to write an ERP software, to compete into markets of two different countries; Italy and Germany. We are going to write many CRUD operations on many domain objects. Nothing challenging here.
But what about modeling country specific business rules, on different data structures, inside the same functionality flow?
For example, both of the countries have articles in their databases and for sure you want to do searches on them. But search rules and fetched data could be totally different.
As data structures are different, we can’t just have the domain entity “Article”: we need at least an “ItalianArticle” and a “GermanArticle”. Consider that data stores could differ also by structure.
Let’s see a Scala implementation: write a sum type
Article
. Then we specialize it as the product types ItalianArticle
and GermanArticle
. Also, we expect that when we search for something, sometimes we found nothing. So, we are going to consider also ArticleNotFound
.sealed trait Article
case class ItaArticle(id: Int, itaData1: String, itaData2: Int) extends Article
case class DeuArticle(id: Int, deuData1: Int, deuData2: Int) extends Article
case class ArticleNotFound() extends Article
Every time that we receive an Article back from somewhere, we are going to pattern match on it. Inside the match, we gain access to its specialized data
def doSomething(article: Article) : Unit = {
article match {
case ita: ItaArticle => print(ita.itaData1)
case deu: DeuArticle => print(deu.deuData2)
case nf: ArticleNotFound => print("None")
}
}
At a first sight, it could look like a classic procedural “switch”, followed by a down-casting. There is an important difference: the compiler is aware if we are matching every type of Article or not. If, for example, we forget to match on
ArticleNotFound
def doSomething(article: Article) : Unit = {
article match {
case ita: ItaArticle => print(ita.itaData1)
case deu: DeuArticle => print(deu.deuData2)
}
}
then by default sbt compiler is going to raise a warning.
As the compiler knows that something is wrong, we can make it raise an error instead of a warning.
At this point, something totally unexpected happens! Business comes to us with a new requirement 😱: we need to distribute also in Spain.
Actually we were prepared, so let’s add
SpaArticle
to our Article
sum type.sealed trait Article
case class ItaArticle(id: Int, itaData1: String, itaData2: Int) extends Article
case class DeuArticle(id: Int, deuData1: Int, deuData2: Int) extends Article
case class SpaArticle(id: Int, spaData1: Float, spaData2: String) extends Article
case class ArticleNotFound() extends Article
Now we need to add Spanish business logic.
With a normal switch, it would be a pain to search for every places where we implemented country specific business rules.
With pattern matching instead, the compiler tell us where we have to intervene.
Yes of course, with the Visitor pattern!
If you don’t know about Visitor pattern, have a look at here
If you don’t know about the Gang of four have a look at here.
Visitor pattern has been known for a long time as an anti-pattern. When you add a new item type, then you are going also to add a new method to the Visitor interface. In this case, the compiler breaks every concrete visitor, until you implement the new method in each of them. This is one of the reasons to be an anti-pattern.
Actually, for our concerns, this “issue” looks like exactly what we are searching for!
Here it follows a Kotlin implementation of our sum types:
interface Article{
fun applyTo(consumer: ArticleConsumer)
}
class ItaArticle(val itaData1: String, val itaData2: Int) : Article {
override fun applyTo(consumer: ArticleConsumer) {
consumer.use(this)
}
}
class DeuArticle(val deuData1: String) : Article {
override fun applyTo(consumer: ArticleConsumer) {
consumer.use(this)
}
}
You can notice that instead of using the “accept” and “Visitor” naming, i prefer to think about “data that are applied to a data consumer”. (I’m searching for a better naming, so if you have suggestions, please leave a comment! Thanks)
And here we can implement country specific business rules
interface ArticleConsumer {
fun use(article: ItaArticle)
fun use(article: DeuArticle)
fun use(article: ArticleNotFound)
}
fun useAnArticle(article: Article) : String {
var x = ""
article.applyTo(object : ArticleConsumer {
override fun use(article: ItaArticle) {
x = article.itaData1
}
override fun use(article: DeuArticle) {
x = article.deuData1
}
override fun use(article: ArticleNotFound) {
x = "not found"
}
})
return x
}
The semantic of this example is totally the same as of FP pattern matching. When we are going to add the new country, then the compiler is going to break everything, until we don’t implement the new
fun use(article: SpaArticle)
in every ArticleConsumer
.The original purpose of the Visitor pattern was to iterate an operation over collections of heterogeneous objects, that doesn’t share the same interface and data types.
In this article I proposed to use it instead as a routing point. It is useful when you end up enumerating your domain entities and need to set a localized domain context.
Enumerating domain entities in methods is argued as being an issue. In this use case, it is the key feature of the pattern.
I also demonstrated that this usage is semantically equal to FP pattern matching.
Please leave your opinion and feedback in comments.
Thank you for reading!