Skip to content

Commit 9f58e36

Browse files
committed
Draft of Replace Conditional with Calculation
1 parent 393fea4 commit 9f58e36

File tree

1 file changed

+147
-0
lines changed

1 file changed

+147
-0
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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

Comments
 (0)