DEV Community

Rafi Panoyan
Rafi Panoyan

Posted on

5

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

fun multiplyBy(multiplier: Int): (Int) -> Int {
return { value -> value * multiplier }
}
val by3 = multiplyBy(3)
val result = by3(5) // 15

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
val filterYears: LiveData<List<Int>> = getRecords.getAll()
.map { records ->
return@map records
.asSequence()
.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]
}
.sorted()
.toList()
}
.asLiveData(job)
val filterMonths: LiveData<List<Int>> = getRecords.getAll()
.map { records ->
return@map records
.asSequence()
.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]
}
.sorted()
.toList()
}
.asLiveData(job)

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 :

private fun newCalendarFieldGetter(calendarField: Int): (Record) -> Int =
{ record ->
Calendar.getInstance().apply {
time = record.date.value
}[calendarField]
}
val filterYears: LiveData<List<Int>> = getRecords.getAll()
.map { records ->
// call the higher-order function
// and get the appropriate getter
val yearFieldGetter = newCalendarFieldGetter(Calendar.YEAR)
return@map records
.asSequence()
// use the appropriate year field getter
.distinctBy(yearFieldGetter)
.map(yearFieldGetter)
.sorted()
.toList()
}
.asLiveData(job)
val filterMonths: LiveData<List<Int>> = getRecords.getAll()
.map { records ->
// same for month
val monthFieldGetter = newCalendarFieldGetter(Calendar.MONTH)
return@map records
.asSequence()
// use the appropriate month field getter
.distinctBy(monthFieldGetter)
.map(monthFieldGetter)
.sorted()
.toList()
}
.asLiveData(job)

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.

private fun newCalendarFieldGetter(calendarField: Int): (Record) -> Int = { record ->
Calendar.getInstance().apply {
time = record.date.value
}[calendarField]
}
private fun getDistinctDateFields(calendarField: Int)
: suspend (List<Record>) -> List<Int> = { records ->
val fieldGetter = newCalendarFieldGetter(calendarField)
records
.asSequence()
.distinctBy(fieldGetter)
.map(fieldGetter)
.sorted()
.toList()
}
val filterYears: LiveData<List<Int>> = getRecords.getAll()
.map(getDistinctDateFields(Calendar.YEAR))
.asLiveData(job)
val filterMonths: LiveData<List<Int>> = getRecords.getAll()
.map(getDistinctDateFields(Calendar.MONTH))
.asLiveData(job)
view raw higher-order.kt hosted with ❤ by GitHub

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.

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (1)

Collapse
 
nameisjayant profile image
Jayant Kumar

love this :)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay