|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: Replace Conditional With Calculation |
| 4 | +tags: |
| 5 | +--- |
| 6 | + |
| 7 | +Among the many refactorings described in Martin Fowler’s Refactoring, "Replace Conditional with Polymorphism" eliminates branching control flow by replacing it with polymorphic calls. An alternative is to replace branching code with straight-line calculations. This is frequently used in graphics and games programming. Unsurprisingly, I call this refactoring "Replace Conditional with Calculation". |
| 8 | + |
| 9 | +## Worked Example |
| 10 | + |
| 11 | +For example, consider the Luhn checksum algorithm that detects mistyped credit card numbers: |
| 12 | + |
| 13 | +```kotlin |
| 14 | +fun String.isValidCardNumber(): Boolean = |
| 15 | + this.reversed() |
| 16 | + .map { ch -> ch.digitToInt() } |
| 17 | + .mapIndexed { index, digit -> |
| 18 | + when (index % 2) { |
| 19 | + 0 -> digit |
| 20 | + else -> digit * 2 |
| 21 | + } |
| 22 | + } |
| 23 | + .sumOf { |
| 24 | + when { |
| 25 | + it >= 10 -> it / 10 + it % 10 |
| 26 | + else -> it |
| 27 | + } |
| 28 | + } |
| 29 | + .let { checkSum -> checkSum % 10 == 0 } |
| 30 | +``` |
| 31 | + |
| 32 | +This has two conditional statements, both of which can be replaced by straight-line integer calculations. |
| 33 | + |
| 34 | +However, although IntelliJ encodes some mathematical reasoning into its refactoring tools, particularly [De Morgan’s laws of boolean algebra](), it does not encode enough arithmetical rules to automatically replace conditional statements with numeric calculations. We have to rely on our own knowledge of arithmetic and the behaviour of Kotlin's integer arithmetic operators to recognise where and how we can replace conditionals with calculations. |
| 35 | + |
| 36 | +To see how, let's start with the first when expression in the function: |
| 37 | + |
| 38 | +```kotlin |
| 39 | +when (index % 2) { |
| 40 | + 0 -> digit |
| 41 | + else -> digit * 2 |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +We can duplicate the `else` branch for the case when `index % 2` is 1, without changing the meaning of the when expression: |
| 46 | + |
| 47 | +```kotlin |
| 48 | +when (index % 2) { |
| 49 | + 0 -> digit |
| 50 | + 1 -> digit * 2 |
| 51 | + else -> digit * 2 |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +Because `index % 2` is either zero or one, the else branch is unreachable code. We can prove this to ourselves by changing the else branch to throw an exception and seeing that our tests still pass: |
| 56 | + |
| 57 | +```kotlin |
| 58 | +when (index % 2) { |
| 59 | + 0 -> digit |
| 60 | + 1 -> digit * 2 |
| 61 | + else -> error("unreachable") |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +Let's make the two branches have the same "shape": |
| 66 | + |
| 67 | +```kotlin |
| 68 | +when (index % 2) { |
| 69 | + 0 -> digit * 1 |
| 70 | + 1 -> digit * 2 |
| 71 | + else -> error("unreachable") |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +We can now lift the multiplication out of the when expression. |
| 76 | + |
| 77 | + |
| 78 | +Note: At the time of writing, IntelliJ can lift _return_ statements out of a conditional expression, but unfortunately it cannot do the same for common subexpressions. We have to do it by hand. |
| 79 | + |
| 80 | + |
| 81 | +```kotlin |
| 82 | +digit * when (index % 2) { |
| 83 | + 0 -> 1 |
| 84 | + 1 -> 2 |
| 85 | + else -> error("unreachable") |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +Now it is obvious that the conditional is calculating `index % 2 + 1`, so we can replace the entire when expression with that calculation: |
| 90 | + |
| 91 | +```kotlin |
| 92 | +digit * (index % 2 + 1) |
| 93 | +``` |
| 94 | + |
| 95 | +This leaves the function as: |
| 96 | + |
| 97 | +```kotlin |
| 98 | +fun String.isValidCardNumber(): Boolean = |
| 99 | + this.reversed() |
| 100 | + .map { ch -> ch.digitToInt() } |
| 101 | + .mapIndexed { index, digit -> digit * (index % 2 + 1) } |
| 102 | + .sumOf { |
| 103 | + when { |
| 104 | + it >= 10 -> it / 10 + it % 10 |
| 105 | + else -> it |
| 106 | + } |
| 107 | + } |
| 108 | + .let { checkSum -> checkSum % 10 == 0 } |
| 109 | +``` |
| 110 | + |
| 111 | +Because we now have one reference to `digit` we can combine the first two map and mapIndexed calls into one: |
| 112 | + |
| 113 | +```kotlin |
| 114 | +fun String.isValidCardNumber(): Boolean = |
| 115 | + this.reversed() |
| 116 | + .mapIndexed { index, ch -> ch.digitToInt() * (index % 2 + 1) } |
| 117 | + .sumOf { |
| 118 | + when { |
| 119 | + it >= 10 -> it / 10 + it % 10 |
| 120 | + else -> it |
| 121 | + } |
| 122 | + } |
| 123 | + .let { checkSum -> checkSum % 10 == 0 } |
| 124 | +``` |
| 125 | + |
| 126 | +The second conditional is an almost direct translation from a description of the Luhn algorithm into Kotlin: "if an intermediate value calculated from a digit of the card number has two digits, add the two digits together, otherwise use the digit". This calculation makes sense if calculating the checksum by pen and paper. But given the behaviour of Kotlin's integer arithmetic operators, the branching is unnecessary in the Kotlin code. If the intermediate value, `it`, is less than ten, then `it/10` would be zero, and `it % 10` would be equal to `it`, meaning we can use the same expression for both branches: |
| 127 | + |
| 128 | +```kotlin |
| 129 | +when { |
| 130 | + it >= 10 -> it / 10 + it % 10 |
| 131 | + else -> it / 10 + it % 10 |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +Therefore, we can replace the entire when expression with `it / 10 + it % 10`, leaving the function as: |
| 136 | + |
| 137 | +```kotlin |
| 138 | +fun String.isValidCardNumber(): Boolean = |
| 139 | + this.reversed() |
| 140 | + .mapIndexed { index, ch -> ch.digitToInt() * (index % 2 + 1) } |
| 141 | + .sumOf { it / 10 + it % 10 } |
| 142 | + .let { checkSum -> checkSum % 10 == 0 } |
| 143 | +``` |
| 144 | + |
| 145 | +## Replace Calculation with Conditional |
| 146 | + |
| 147 | +The intent of a calculation can be harder to understand than of explicit conditional code. Just as you can refactor between conditionals and polymorphism in either direction, from conditionals to polymorphism or from polymorphism to conditionals, so you can refactor between conditionals and calculations. |
0 commit comments