Scala Programming Projects
上QQ阅读APP看书,第一时间看更新

Refactoring the retirement calculator to use Option

Now that we know what Option can do for us, we are going to refactor one of the functions of the retirement calculator that we developed in Chapter 2Developing a Retirement Calculator, to improve the handling of some edge-case scenarios. If you have not done it yet, please follow the instructions at the beginning of the Chapter 2Developing a Retirement Calculator, to set up the project.

In RetCalc.scala, we are going to change the return type of nbMonthsSaving. In Chapter 2Developing a Retirement Calculator, we returned Int.MaxValue if netIncome <= currentExpense to avoid looping infinitely. This was not very robust, as this infinite result could then be used in another computation, which would lead to bogus results. It would be better to return Option[Int] to indicate that the function might not be computable and let the caller decide what to do. We would return None if it was not computable or Some(returnValue) if it was computable.

The following code is the new implementation for nbMonthsSaving, with the changed portions highlighted in bold:

def nbOfMonthsSaving(params: RetCalcParams, 
returns: Returns): Option[Int] = {
import params._
@tailrec
def loop(months: Int): Int = {
val (capitalAtRetirement, capitalAfterDeath) =
simulatePlan(returns, params, months)

if (capitalAfterDeath > 0.0)
months
else
loop(months + 1)
}

if (netIncome > currentExpenses)
Some(loop(0))
else
None
}

Now try to compile the project. This change breaks many parts of our project, but the Scala compiler is a terrific assistant. It will help us identify the portions of the code we need to change to make our code more robust.

The first error is in RetCalcSpec.scala, as shown in the following code:

Error:(65, 14) types Option[Int] and Int do not adhere to the type constraint selected for the === and !== operators; the missing implicit parameter is of type org.scalactic.CanEqual[Option[Int],Int]
actual should ===(expected)

This error means that the types in the actual should === (expected) expression do not match: actual is of the Option[Int]0 type, whereas expected is of the Int type. We need to change the assertion, as follows:

actual should ===(Some(expected))

You can apply the same fix for the second unit test. For the last unit test, we want to assert that None is returned instead of Int.MaxValue, as shown in the following code:

"not loop forever if I enter bad parameters" in {
val actual = RetCalc.nbOfMonthsSaving(params.copy(netIncome = 1000), FixedReturns(0.04))
actual should ===(None)
}

You can now compile and run the test. It should pass.

You are now able to model an optional value safely. However, sometimes it is not always obvious to know what None actually means. Why did this function return None? Was it because the arguments that were passed were wrong? Which argument was wrong? And what value would be correct? It would indeed be nice to have some explanation that comes along with the None in order to understand why there was no value. In the next section, we are going to use the Either type for this purpose.