A couple of years ago, I wrote a post focused on how to avoid sequences of if-else statements. In that post, I demo several alternatives:
- the usage of proper OOP design
- maps
- when there’s no
return
,switch
statements in acase
.
Recently, I stumbled upon a slightly more complex use-case. This post describes it, and details what additional options are available in Kotlin.
Modeling a simple if… else sequence
Let’s start with modeling a simple sequence of if-else statements. Imagine we need to check the value of a letter to return an integer associated with the later.
As mentioned in the previous post, an easy alternative to if-else is to create a map, with letters as keys, and integers as values.
val mappings = mapOf(
"A" to 1,
"B" to 2,
"C" to 3,
"D" to 4,
"E" to 5,
"F" to 6,
"G" to 7
)
val result = mappings["A"]
Remember to always use immutable types for keys! |
Making the problem more complex
Now imagine that instead of checking one value, one needs to check two different inputs to compute the return value.
The straightforward way to solve this is with embedded if
:
if (input1 == "A") {
if (input2 == "A") return 1
if (input2 == "B") return 2
if (input2 == "C") return 3
// and the rest
} else if (input1 == "B") {
if (input2 == "A") return 2
if (input2 == "B") return 4
if (input2 == "C") return 6
// and the rest
}
// and the rest
With the maps approach, this would be akin to:
val mappings = mapOf(
"A" to mapOf(
"A" to 1,
"B" to 2,
"C" to 3
// and the rest
),
"B" to mapOf(
"A" to 2,
"B" to 4,
"C" to 6
// and the rest
)
// and the rest
)
val result = mappings["A"]["B"]
I believe the previous code is hard to read:
- Creating the map requires a lot of nested code
- Getting the result is a two-step process - first get a one-dimensional map, then the scalar value
- The order of the keys shouldn’t be mixed up:
mappings["A"]["B"]
is (probably) different frommappings["B"]["A"]
Solving the complex problem
Let’s stop for a second, and model the first sequence as an array:
A | B | C | D | E | F | G |
---|---|---|---|---|---|---|
1 |
2 |
3 |
4 |
5 |
6 |
7 |
The above solution has one dimension. Imagine now that the choices are a 2D matrix:
A | B | C | D | E | F | G | |
---|---|---|---|---|---|---|---|
A |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
B |
2 |
4 |
6 |
8 |
10 |
12 |
14 |
C |
3 |
6 |
9 |
12 |
15 |
18 |
21 |
D |
4 |
8 |
12 |
16 |
20 |
24 |
28 |
E |
5 |
10 |
15 |
20 |
25 |
30 |
35 |
F |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
G |
7 |
14 |
21 |
28 |
35 |
42 |
49 |
This is akin to a multimap, a map with two keys.
Neither Java nor Kotlin has a Multimap
type, but libraries do:
- For example, there’s one in Guava
- And another one in Apache Commons Lang
With Kotlin, there’s no need for a multimap, up to 3 dimensions, thanks to the Pair
and Triple
classes.
val mappings = mapOf(
("A" to "A") to 1,
("A" to "B") to 2,
("A" to "C") to 3,
("B" to "A") to 2,
("B" to "B") to 4,
("B" to "C") to 6
// and the rest
)
val result = mappings["A" to "B"]
For keys with a dimension higher than 3, create a dedicated immutable type, and override its equals() and hashCode() methods.
|
Returning computations
In the old post, a switch
was used when the code didn’t return a result, but invoked a method instead:
when(key) {
"A" to "A" -> doSomething()
"A" to "B" -> doSomethingElse()
"A" to "C" -> andNowForSomethingCompletelyDifferent()
}
However, it’s an even better alternative to store the computation in the map. It can then be invoked dynamically:
val mappings = mapOf(
("A" to "A") to { doSomething() },
("A" to "B") to { doSomethingElse() }
("A" to "C") to { andNowForSomethingCompletelyDifferent() }
)
mappings["A" to "B"]?.invoke()
The only requirement is that all computations have the same signature: here, a function that takes no input parameter.
The return value is not used in the above code. If it does, it can be captured easily:
val result = mappings["A" to "B"]?.invoke()
If a default value is needed when no key matches, the getOrDefault()
function is your friend:
val mappings = mapOf(
("A" to "A") to { doSomething() },
("A" to "B") to { doSomethingElse() }
("A" to "C") to { andNowForSomethingCompletelyDifferent() }
)
val result = mappings.getOrDefault("A" to "E") { doDefault() }.invoke()
Conclusion
In the earlier post, I listed some simple techniques to avoid sequences of if-else statements that were relevant for simple use-cases. In this one, I showed more advanced techniques that adress more advanced usages: multimaps for nested evaluations, and storing the computation as a value in the map when code execution is required.
Improving code readability should be a number one priority when writing code. It doesn’t require fancy techniques, just a constant drive to achieve it. Knowing and understanding one’s language and available libraries goes a long way toward that goal.