Easy way to write clean code using Higher-Order Functions and HashMaps
In one of my previous articles, I talked about Test-Driven Development. There we ended up with a solution having a switch-case. In this one, we will see ways to avoid having a switch-case or an if-else ladder.
I will start with a problem known as Reverse Polish Notation. A lot of you might have heard of this problem, and it is a straightforward problem to solve. But our focus for this article is to understand how we can write CLEAN CODE by removing the if-else
ladder by using Hashmaps and Higher-Order functions.
Before we start, the solution discussed here is in Java and Kotlin.
What is Reverse Polish Notation:
In reverse Polish notation, the operators follow their operands; for instance, to add 3 and 4, one would write _3 4 +_
rather than _3 + 4_
. If there are multiple operations, operators are given immediately after their second operands; so the expression is written _3 − 4 + 5_
in conventional notation would be written _3 4 − 5 +_
in reverse Polish notation: 4 is first subtracted from 3, then 5 is added to it.
The solution to this problem is also pretty simple. We need to use a Stack and continuously push the elements until we find any operator (+, –, *, /)
. Once we find an operator we pop the two numbers from the stack and perform the operation (Addition, Division, etc.) on them.
So, for input 3 4 +
we will push 3, and then push 4 and then when we encounter a +
we will pop 3
and 4
and add them.
Naive Solution
A naive solution in Java would look like this. We have a method evaluate
which takes in the input, splits it using “ ”
. And iterate over the char array pushing numbers to the stack and applying operations based on the operators encountered using the switch-case.
import java.util.Stack;
class ReversePolishNotation {
public static void main(String[] args)
{
System.out.println(evaluate("5 1 2 + 4 * + 3 -"));
}
public static double evaluate(String expr)
{
String[] digitString = expr . split (" ");
Stack<Float> stack = new Stack<Float>();
for (String s : digitString) {
switch(s) {
case "+": {
float numberOne = stack . pop ();
float numberTwo = stack . pop ();
stack.push(numberOne + numberTwo);
break;
}
case "-": {
float numberOne = stack . pop ();
float numberTwo = stack . pop ();
stack.push(numberTwo - numberOne);
break;
}
case "*": {
float numberOne = stack . pop ();
float numberTwo = stack . pop ();
stack.push(numberOne * numberTwo);
break;
}
case "/": {
float numberOne = stack . pop ();
float numberTwo = stack . pop ();
stack.push(numberTwo / numberOne);
break;
}
default: {
stack.push(Float.parseFloat(s));
break;
}
}
}
return stack.pop();
}
}
This is the first iteration of the solution that works as expected. Now, let's think of a better solution to this. If you look at each of the cases we see a lot of duplication.
float numberOne = stack.pop();
float numberTwo = stack.pop();
stack.push(numberOne + numberTwo);
The common part in the above code is the stack operation. The only change is the operation that we perform. If we could parameterize the operation to a function we should be able to reuse the code. Let’s see how we can do that.
Using BiFunction (Higher-Order Function)
The [BiFunction](https://docs.oracle.com/javase/8/docs/api/java/util/function/BiFunction.html)
is a specialization of the [Function](https://learnjava.co.in/java-8-function-interface-example/)
interface that accepts 2 arguments. Just like a Function
, it provides a method called apply
. This method accepts 2 arguments of any data type and returns a result. Exactly what we need to perform the operations. Our add function takes in two parameters and returns the result back!
So we can pass addition, subtraction, multiplication, and division functions as BiFunctions.
import java.util.Stack;
import java.util.function.BiFunction;
class ReversePolishNotation {
public static void main(String[] args)
{
System.out.println(evaluate("4 2 /"));
}
public static double evaluate(String expr)
{
String[] digitString = expr . split (" ");
Stack<Float> stack = new Stack<Float>();
for (String s : digitString) {
switch(s) {
case "+": {
stack.push(operate((a, b) -> a+b, stack));
break;
}
case "-": {
stack.push(operate((a, b) -> a-b, stack));
break;
}
case "*": {
stack.push(operate((a, b) -> a * b, stack));
break;
}
case "/": {
stack.push(operate((a, b) -> a / b, stack));
break;
}
default: {
stack.push(Float.parseFloat(s));
break;
}
}
}
return stack.pop();
}
public static Float operate(BiFunction<Float, Float, Float> function, Stack<Float> stack)
{
float numberOne = stack.pop();
float numberTwo = stack.pop();
return function.apply(numberTwo, numberOne);
}
}
In the above code the BiFunction is:
(a, b) -> a + b
And our method operate
takes in the BiFunction as the input.
Now, we have removed the duplication by extracting a function from it and making it accept our operations as functions.
But there is still that switch-case ladder which is taking care of figuring out which operation we should use. What if we could get rid of that?
Let's think about that. Only if we could do a operator
lookup, we would be able to remove this ladder.
And what can be used to do a lookup?
A HashMap indeed!
Removing The Switch Case
We map our operators with the functions and then when we iterate over the string, and fetch functions if we find an operator otherwise we just push that number to the stack.
import java.util.HashMap;
import java.util.Stack;
import java.util.function.BiFunction;
class ReversePolishNotation {
public static void main(String[] args)
{
System.out.println(evaluate("4 2 /"));
}
public static double evaluate(String expr)
{
String[] digitString = expr . split (" ");
Stack<Float> stack = new Stack<>();
HashMap<String, BiFunction<Float, Float, Float>> map = constructMapForOperator ();
for (String s : digitString) {
if (map.containsKey(s)) {
stack.push(operate(map.get(s), stack));
} else {
stack.push(Float.parseFloat(s));
}
}
return stack.pop();
}
private static HashMap<String, BiFunction<Float, Float, Float>> constructMapForOperator()
{
HashMap<String, BiFunction<Float, Float, Float>> map = new HashMap();
map.put("+", (a, b) -> a+b));
map.put("-", (a, b) -> a-b);
map.put("*", (a, b) -> a * b);
map.put("/", (a, b) -> a / b);
return map;
}
public static Float operate(BiFunction<Float, Float, Float> function, Stack<Float> stack)
{
float numberOne = stack.pop();
float numberTwo = stack.pop();
return function.apply(numberTwo, numberOne);
}
}
This approach can be utilized in all those places where you see an if-else or a switch-case ladder. This works almost everywhere. You just have to carefully think about the Higher-Order function that you need to create.
For the Kotlin enthusiasts out there, here is the same solution in Kotlin.
import java.util.*
fun main() {
println(evaluate("4 2 /"))
}
fun evaluate(expr: String): Float {
val chars = expr.split(" ")
val stack = Stack<Float>()
val operator = operationForOperator()
for (c in chars) {
operator[c]?.let { stack.push(operate(it, stack)) } ?: stack.push(c.toFloat())
}
return stack.pop()
}
fun operationForOperator(): Map<String, (Float, Float) -> Float> {
return mapOf(
"+" to { a, b -> a + b },
"-" to { a, b -> a - b },
"*" to { a, b -> a * b },
"/" to { a, b -> a / b }
)
}
fun operate(function: (a: Float, b: Float) -> Float, stack: Stack<Float>): Float {
val numberOne = stack.pop()
val numberTwo = stack.pop()
return function(numberTwo, numberOne)
}
If you notice, we do not need an if-else here as in the for-loop at the top. We can utilize Kotlin’s Elvis Operator (?:) which will automatically execute the last part if there is no matching key in the map. We started with 50 lines of code and reached 30 lines of code using Kotlin.
You can follow me on Twitter where I occasionally post from my learnings.