Pattern matching

Now that we have a way of expressing variable returns, we need to change our futureCapital function to accept a Returns type instead of a monthly interest rate of type Double. Change the tests first:

"RetCalc.futureCapital" should {
"calculate the amount of savings I will have in n months" in {
// Excel =-FV(0.04/12,25*12,1000,10000,0)
val actual = RetCalc.futureCapital(FixedReturns(0.04),
nbOfMonths = 25 * 12, netIncome = 3000,
currentExpenses = 2000, initialCapital = 10000).right.value
val expected = 541267.1990
actual should ===(expected)
}

"calculate how much savings will be left after having taken a
pension for n months" in {
val actual = RetCalc.futureCapital(FixedReturns(0.04),
nbOfMonths = 40 * 12, netIncome = 0, currentExpenses = 2000,
initialCapital = 541267.198962).right.value
val expected = 309867.5316
actual should ===(expected)
}
}

Then, change the futureCapital function in RetCalc as follows:

def futureCapital(returns: Returns, nbOfMonths: Int, netIncome: Int, currentExpenses: Int,
initialCapital: Double): Double = {
val monthlySavings = netIncome - currentExpenses
(0 until nbOfMonths).foldLeft(initialCapital) {
case (accumulated, month) =>
accumulated * (1 + Returns.monthlyRate(returns, month)) +
monthlySavings
}
}

Here, instead of just using the interestRate in the formula, we introduced a new function called Returns.monthlyRate which we must now create. As we follow a rather strict TDD approach, we will only create its signature first, then write the unit test, and finally implement it.

Write the function signature in Returns.scala:

object Returns {
def monthlyRate(returns: Returns, month: Int): Double = ???
}

Create a new unit test ReturnsSpec in the retcalc package in src/test/scala:

class ReturnsSpec extends WordSpec with Matchers with TypeCheckedTripleEquals {

implicit val doubleEquality: Equality[Double] =
TolerantNumerics.tolerantDoubleEquality(0.0001)

"Returns.monthlyRate" should {
"return a fixed rate for a FixedReturn" in {
Returns.monthlyRate(FixedReturns(0.04), 0) should ===(0.04 / 12)
Returns.monthlyRate(FixedReturns(0.04), 10) should ===(0.04 / 12)
}

val variableReturns = VariableReturns(Vector(
VariableReturn("2000.01", 0.1),
VariableReturn("2000.02", 0.2)))
"return the nth rate for VariableReturn" in {
Returns.monthlyRate(variableReturns, 0) should ===(0.1)
Returns.monthlyRate(variableReturns, 1) should ===(0.2)
}

"roll over from the first rate if n > length" in {
Returns.monthlyRate(variableReturns, 2) should ===(0.1)
Returns.monthlyRate(variableReturns, 3) should ===(0.2)
Returns.monthlyRate(variableReturns, 4) should ===(0.1)
}
}

These tests act as a specification for our monthlyRate function. For VariableRate, the monthlyRate must return the nth rate stored in the returned Vector. If n is greater than the number of rates, we decide that monthlyRate should go back to the beginning of Vector, as if the history of our variable returns would repeat itself infinitely. We could have made a different choice here, for instance, we could have taken a mirror of the returns, or we could have just returned some error if we reached the end. To implement this rotation, we are taking the month value and applying the modulo ( % in Scala ) of the length of the vector.

The implementation introduces a new element of syntax, called pattern matching:

def monthlyRate(returns: Returns, month: Int): Double = returns match {
case FixedReturns(r) => r / 12
case VariableReturns(rs) => rs(month % rs.length).monthlyRate
}

You can now run ReturnsSpec, and all tests should pass. Pattern matching allows you to deconstruct an ADT and evaluate some expression when it matches one of the patterns. You can also assign variables along the way and use them in the expression. In the preceding example, case FixedReturns(r) => r/12 can be interpreted as "if the variable returns is of type FixedReturns, assign r = returns.annualRate, and return the result of the expression r/12".

This is a simple example, but you can use much more complicated patterns. This feature is very powerful, and can often replace lots of if/else expressions. You can try some more complex patterns in the Scala Console:

scala> Vector(1, 2, 3, 4) match {
case head +: second +: tail => tail
}
res0: scala.collection.immutable.Vector[Int] = Vector(3, 4)

scala> Vector(1, 2, 3, 4) match {
case head +: second +: tail => second
}

scala> ("0", 1, (2.0, 3.0)) match {
case ("0", int, (d0, d1)) => d0 + d1
}
res2: Double = 5.0

scala> "hello" match {
case "hello" | "world" => 1
case "hello world" => 2
}
res3: Int = 1

scala> def present(p: Person): String = p match {
case Person(name, age) if age < 18 => s"$name is a child"
case p => s"${p.name} is an adult"
}
present: (p: Person)String

It is a good practice to exhaustively match all possible patterns for your value. Otherwise, if no pattern matches the value, Scala will raise a runtime exception, and it might crash your program. However, when you use sealed traits, the compiler is aware of all the possible classes for a trait and will issue a warning if you do not match all cases.

In Returns.scala, try to comment out this line with cmd + /:

  def monthlyRate(returns: Returns, month: Int): Double = returns match {
// case FixedReturns(r) => r / 12
case VariableReturns(rs) => rs(month % rs.length).monthlyRate
}

Recompile the project with cmd + F9. The compiler will warn you that you are doing something wrong:

Warning:(27, 59) match may not be exhaustive.
It would fail on the following input: FixedReturns(_)
def monthlyRate(returns: Returns, month: Int): Double = returns match {

If you then try to remove the sealed keyword and recompile, the compiler will not issue any warning.

We now have a good grasp of how to use pattern matching. Keep the sealed keyword, revert the comment in monthlyRate, and run ReturnsSpec to make sure everything is green again.

If you are coming from an object-oriented language, you might wonder why we did not implement monthlyRate using an abstract method with implementations in FixedRate and VariableRate. This is perfectly feasible in Scala, and some people might prefer this design choice.

However, as I am an advocate of a functional programming style, I prefer using pure functions in objects:

  • They are easier to reason about, as the whole dispatching logic is in one place.
  • They can be moved to other objects easily, which facilitates refactoring.
  • They have a more limited scope. In class methods, you always have all the attributes of the class in scope. In a function, you only have the parameters of the function. This helps unit testing and readability, as you know that the function cannot use anything else but its parameters. Also, it can avoid side effects when the class has mutable attributes.
  • Sometimes in object-oriented design, when a method manipulates two objects, A and B, it is not clear if the method should be in class A or class B.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset