DEV Community

Rafi Panoyan
Rafi Panoyan

Posted on

Higher-order functions in Kotlin

Kotlin is a language where functions are first class citizens. This allow us to manipulate them as we please.

Today we will see what higher-order functions are, why do they matter and how to create them in Kotlin.

My functions are High

In functional programming (FP for the rest of the post), as the name suggests, functions are the base building blocks of the code.

Often in OOP, a function takes some data as an input, process them, and returns an object (representing some other data).

In FP you can also find higher-order functions : functions that returns another function or functions that take one or more function as arguments.

Show me the code

Step by step, here is what's happening :

fun multiplyBy(multiplier: Int): (Int) -> Int {
    return { value -> value * multiplier }
}

If we look at the multiplyBy function signature and return type, we can tell that :

  • it takes an Int
  • it returns a function that takes an Int and returns an Int.

Returning a function is a typical signature of a higher-order function.

The multiplyBy body consist of returning a new function which takes a value in parameter and returns that value multiplied by our multiplier.

val by3 = multiplyBy(3)

Here we call our higher-order function to create a new function named by3 which will multiply by 3 every parameter we give it.

val result = by3(5)

Clearly, we execute by3 with 5 as a parameter. The result is 15.

Ok but, why ?!

If you would have asked me this question yesterday, I would have said... I don't know.

I always liked this "function that creates other function" principle in FP, but never seemed to exceed the academic meaning of it.

Yes, you can now write this :

val by5 = multiplyBy(5)

but in a real application, what's the matter ?

Unless...

My functions are Small

After many years studying FP, writing higher-order functions (HOF for the rest of the post) in Clojure, Haskell or Kotlin, just for the sake of learning a new language, I finally came across a real use case where it saved me a good amount of duplicate code.

I will now develop a concrete (and very simplified) example from an Android application I'm developping.

Context

This application holds a list of dated objects (Record). The user should be able to filter these records by year and by month independently.

Given this list :

  • Record 1 : 1st january 2019
  • Record 2 : 1st march 2020
  • Record 3 : 1st may 2020

I should get these filters :

Years : 2019 2020

Months : january march may

From a single source of data I must get 2 distinct lists based on 2 distinct fields of the record's date (the year and the month).

First try

I expose these filters to my UI with two different LiveData, one for the years and one for the months (if you don't know what a LiveData is, just think about them as Observable data).

The general idea of the flow is as follows :

  • get the list of all records with getRecords.getAll()
  • map this list from List<Record> to List<Int> representing the list of years or months of the filters
    • we call distinctBy to obtain a list of distinct records based on their year or month
    • we map this new list to extract only the corresponding field of the date
    • we sort the final list of years or months

This piece of code has several issues. One of them being that the two filters have almost exactly the same code, and only this part differs :

.distinctBy { record ->
    return@distinctBy Calendar.getInstance().apply {
        time = record.date.value
    }[Calendar.YEAR]
}
.map { record ->
    return@map Calendar.getInstance().apply {
        time = record.date.value
    }[Calendar.YEAR]
}

.distinctBy { record ->
    return@distinctBy Calendar.getInstance().apply {
        time = record.date.value
    }[Calendar.MONTH]
}
.map { record ->
    return@map Calendar.getInstance().apply {
        time = record.date.value
    }[Calendar.MONTH]
}

This seems to be a good candidate for a refact.

Quick note

These functions (map, distinctBy, filter, etc.) are all HOF as well, because they take another function as argument (here it's the lambdas we are passing). They implement Function composition and it's one of the main use of HOF.

The OOP-way

Instinctively in OOP, I would have created a function taking two parameters : a record and the calendar field to read from the Calendar instance.

private fun readCalendarField(record: Record, calendarField: Int) {
    return Calendar.getInstance().apply {
        time = record.date.value
    }[calendarField]
}

We would use it as follows :

// years filter
.distinctBy { record ->
    return@distinctBy readCalendarField(record, Calendar.YEAR)
}
.map { record ->
    return@map readCalendarField(record, Calendar.YEAR)
}

// months filter
.distinctBy { record ->
    return@distinctBy readCalendarField(record, Calendar.MONTH)
}
.map { record ->
    return@map readCalendarField(record, Calendar.MONTH)
}

Better. We avoided the duplicate implementation of reading the calendar field, but we still have duplication.

For each filter, the exact same lambda has been created twice : one for distinctBy and another for map. How can we avoid that ?

The FP-way

You saw it coming, let's introduce our higher-order function !

We need to write a function that creates a function, very similar to the lambda we were creating twice, but with an additional parameter : the calendar field.

private fun newCalendarFieldGetter(calendarField: Int): (Record) -> Int = 
    { record ->
        Calendar.getInstance().apply {
            time = record.date.value
        }[calendarField]
    }

The return type of our function is key here : (Record) -> Int. It complies to both distinctBy and map input type which is (T) -> R.

Additionnaly, our HOF asks for a calendar field to parameterize the new function that it creates.

We can now write this :

This starts to look pretty good ! We take advantage of the fact that newCalendarFieldGetter is a pure function (thus has no side effects) to share the same instance for dinstinctBy and map.

Now let's tackle the last duplication of this snippet : the two filters are exactly identical, only differing by the calendar field parameter.

Following the exact same logic as previously, we can create a parameterized function to fill the getRecords.getAll().map() input.

Perfection

The map function we are trying to call needs one parameter of type suspend (T) -> R.

Our new higher-order function will comply to this by returning a suspend (List<Record>) -> List<Int>, and it will take one more parameter, the calendar field.

private fun getDistinctDateFields(calendarField: Int)
    : suspend (List<Record>) -> List<Int> = { records ->
        val fieldGetter = newCalendarFieldGetter(calendarField)
        records
            .asSequence()
            .distinctBy(fieldGetter)
            .map(fieldGetter)
            .sorted()
            .toList()
    }

The previously duplicated block is now generic and can be used to extract a list of any disctinct field of the Record date !

Here is how the entire code looks like.

With this version, adding a third filter on another field of the date, let's say DAY_OF_MONTH, is quit easy !

Final thoughts

This kind of refact is not the most impressive thing you can do with HOF, but it was simple enough to understand how it works in Kotlin and how to use them with common collection manipulation functions as map, filter etc, which also are higher-order functions.

Function composition is a frequent use of HOF, I will certainly post an article about this in the future.

The goal is not to use them everywhere. A good balance must be found between readability, maintainability and optimization (as every aspect of programming). However it's always interesting to learn more new tools to helps us choose the right one for every task.

P.S.

A more optimized version of the getDistinctDateFields function consists of mapping first all records to the date field, and call distinct() right after. It eliminates the need to use the same fieldGetter instance twice :

private fun getDistinctDateFields(calendarField: Int)
    : suspend (List<Record>) -> List<Int> = { records ->
        records
            .asSequence()
            .map(newCalendarFieldGetter(calendarField))
            .distinct()
            .sorted()
            .toList()
    }

That being said, I won't modify the post to include this new version because it allows us to see the possibility of this kind of "function instance" use.

Top comments (1)

Collapse
 
nameisjayant profile image
Jayant Kumar

love this :)