July 25, 2020
This post is about the transition from Kotlin sourcecode that looks like java code to code that uses the functional Kotlin constructs.
Starting up
I will write about a completely made up class here but I experienced that you can do that with almost any class in the entire codebase.
Inspiration from the daily programmer: word funnel
Starting with Java
We start with the java implementation of the word funnel puzzle mentioned above
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static java.util.stream.Collectors.toList;
public class WordFunnel {
public static void main(final String... args) {
if(funnel("leave","eave")){
System.out.println("funnel");
}
}
private static boolean funnel(final String first, final String second) {
final List<Integer> firstList = first.chars().boxed().collect(toList());
for (int i = 0; i < firstList.size(); i++) {
final List<Integer> tempList = new ArrayList<>(firstList);
tempList.remove(i);
if (join(tempList).equals(second)) {
return true;
}
}
return false;
}
private static String join(final List<Integer> tempList) {
final List<String> chars = tempList.stream().map(x->(char)x.intValue()).map(Objects::toString).collect(toList());
return String.join("", chars);
}
}
Transform it to Kotlin
import java.lang.String.join
import java.util.*
import java.util.stream.Collectors.toList
fun main(args: Array<String>) {
if (funnel("leave", "eave")) {
System.out.println("funnel")
}
}
private fun funnel(first: String, second: String): Boolean {
val firstList = first.chars().boxed().collect(toList())
for (i in 0..firstList.size) {
val tempList = getSublist(firstList, i)
if (joinChars(tempList) == second) {
return true
}
}
return false
}
private fun getSublist(firstList: List<Int>, i: Int): List<Int> {
val tempList = ArrayList<Int>(firstList)
tempList.removeAt(i)
return tempList
}
private fun joinChars(tempList: List<Int>): String {
val chars = tempList.stream().map { it.toChar().toString() }.collect(toList())
return join("", chars)
}
The main difference is, that we dont have the class construct anymore and start the main()
function directly.
We also changed the implementation of the getSublist()
function and switched remove()
to removeAt()
.
Start the cleansing
We start to optimize with the getSublist()
function. We remove the local variable and use the Kotlin .apply{}
construct here:
private fun getSublist(firstList: List<Int>, i: Int): List<Int> {
return ArrayList<Int>(firstList).apply {
removeAt(i)
}
}
Now that this function only contains one call we can make it a single line function:
private fun getSublist(firstList: List<Int>, i: Int) =
ArrayList<Int>(firstList).apply {
removeAt(i)
}
We apply the same treatment to the joinChars()
function:
private fun joinChars(tempList: List<Int>) =
tempList.stream().map { it.toChar().toString() }.collect(toList()).let {
join("", it)
}
Use extension functions to spare a parameter
We change joinChars()
to an extension function for the generic class List<Int>
to be able to call it directly:
private fun funnel(first: String, second: String): Boolean {
val firstList = first.chars().boxed().collect(toList())
for (i in 0..firstList.size) {
val tempList = getSublist(firstList, i)
if (tempList.joinChars() == second) {
return true
}
}
return false
}
private fun getSublist(firstList: List<Int>, i: Int) =
ArrayList<Int>(firstList).apply {
removeAt(i)
}
private fun List<Int>.joinChars() =
stream().map { it.toChar().toString() }.collect(toList<String>()).let {
join("", it)
}
Now we can ommit the tempList
variable in the funnel()
method:
private fun funnel(first: String, second: String): Boolean {
val firstList = first.chars().boxed().collect(toList())
for (i in 0..firstList.size) {
if (getSublist(firstList,i).joinChars() == second) {
return true
}
}
return false
}
We also refactor the getSublist()
method into an extension function of List<Int>
:
private fun funnel(first: String, second: String): Boolean {
val firstList = first.chars().boxed().collect(toList())
for (i in 0..firstList.size) {
if (firstList.getSublist(i).joinChars() == second) {
return true
}
}
return false
}
private fun List<Int>.getSublist(i: Int) =
ArrayList<Int>(this).apply {
removeAt(i)
}
Refactor the for loop
We can remove the last local variable in the funnel()
function and replace it with a .run{}
call:
private fun funnel(first: String, second: String): Boolean {
first.chars().boxed().collect(toList()).run {
for (i in 0..size) {
if (getSublist(i).joinChars() == second) {
return true
}
}
return false
}
}
Inside the .run{}
block this
is now bound to the previous variable firstList
and though we can omit it at the calls of the extension functions.
We can now also make this method singleline:
private fun funnel(first: String, second: String) =
first.chars().boxed().collect(toList()).run {
for (i in 0..size) {
if (getSublist(i).joinChars() == second) {
return true
}
}
false
}
The two different return notations look quite ugly.
To get rid of these and replace the for loop we can use the (0..size).any{}
construct:
private fun funnel(first: String, second: String) =
first.chars().boxed().collect(toList()).run {
(0..size).any {
getSublist(it).joinChars() == second
}
}
Wrap it up
As you can see our general goal was to reduce complexity by removing local variables. To archieve this we used the following techniques:
- Kotlin standard functions
apply.{}
andlet.{}
, see Mastering Kotlin standard functions for more information - Single line functions to get rid of that pair of parens
- Extension functions to spare a parameter, see Extension functions for more information
Gist source code: WordFunnel