Currying in Kotlin: A Comprehensive Guide to Streamlining Your Code for Enhanced Functionality and Efficiency

Table of Contents

Currying is a programming technique that involves transforming a function that takes multiple arguments into a series of functions that take a single argument. In other words, it’s a way of breaking down a complex function into smaller, simpler functions that can be composed together to achieve the same result. In this blog post, we will explore the concept of currying in Kotlin and how it can be used to write more concise and expressive code.

What is Currying?

Currying is named after Haskell Curry, a mathematician who introduced the concept in the 20th century. At its core, currying is a way of transforming a function that takes multiple arguments into a series of functions that each take a single argument. For example, consider the following function:

Kotlin
fun add(a: Int, b: Int): Int {
    return a + b
}

This function takes two arguments, a and b, and returns their sum. With currying, we can transform this function into two nested functions that each take a single argument:

Kotlin
fun addCurried(a: Int): (Int) -> Int {
    return fun(b: Int): Int {
        return a + b
    }
}

Now, instead of calling add(a, b), we can call addCurried(a)(b) to achieve the same result. The addCurried function takes a single argument a and returns a new function that takes a single argument b and returns the sum of a and b.

Why Use Currying?

Currying may seem like a simple concept, but it has a number of advantages when it comes to writing code. Here are a few benefits of using currying:

  1. Simplify Complex Functions: Currying allows you to break down complex functions into smaller, simpler functions that are easier to understand and reason about. By focusing on one argument at a time, you can more easily test and debug your code.
  2. Reusability: Currying allows you to reuse functions more easily. By defining a function that takes a single argument and returns a new function, you can create reusable building blocks that can be combined in different ways to achieve different results.
  3. Composition: Currying allows you to compose functions more easily. By breaking down complex functions into smaller, simpler functions, you can combine them in different ways to achieve more complex behaviors.

Examples of Currying in Kotlin

Let’s take a look at some examples of currying in Kotlin to see how it can be used in practice.

1. Adding Numbers

Kotlin
fun addCurried(a: Int): (Int) -> Int {
    return fun(b: Int): Int {
        return a + b
    }
}

val add5 = addCurried(5)
val add10 = addCurried(10)

println(add5(3)) // prints "8"
println(add10(3)) // prints "13"

In this example, we define a curried version of the add function that takes a single argument a and returns a new function that takes a single argument b and returns the sum of a and b. We then create two new functions, add5 and add10, by calling addCurried with the values 5 and 10, respectively. We can then call these functions with a single argument to achieve the same result as calling the original add function with two arguments.

2. Filtering Lists

Kotlin
fun filterCurried(predicate: (Int) -> Boolean): (List<Int>) -> List<Int> {
    return fun(list: List<Int>): List<Int> {
        return list.filter(predicate)
    }
}

val isEven = { n: Int -> n % 2 == 0 }
val isOdd = { n: Int -> n % 2 != 0 }

val filterEven = filterCurried(isEven)
val filterOdd = filterCurried(isOdd)

val numbers = listOf(1, 2, 3, 4, 5, 6)

println(filterEven(numbers)) // prints "[2, 4, 6]"
println(filterOdd(numbers)) // prints "[1, 3, 5]"

In this example, we define a curried version of the filter function that takes a single argument, predicate, and returns a new function that takes a list of integers and returns a new list containing only the elements that satisfy the predicate.

We then define two predicates, isEven and isOdd, that return true if a given number is even or odd, respectively. We create two new functions, filterEven and filterOdd, by calling filterCurried with isEven and isOdd, respectively. Finally, we call these functions with a list of integers to filter the even and odd numbers from the list.

Partial Application

One important concept related to currying is partial application. Partial application refers to the process of fixing some arguments of a function to create a new function with fewer arguments. This can be accomplished by calling a curried function with some, but not all, of its arguments. The resulting function is a partially applied function that can be called later with the remaining arguments.

For example, suppose we have a curried function sumCurried that takes two arguments and returns their sum. We can create a new function add3 that adds 3 to any number by partially applying sumCurried with the argument 3 as follows:

Kotlin
fun sumCurried(x: Int): (Int) -> Int = { y -> x + y }
val add3 = sumCurried(3)

Now add3 is a new function that takes a single argument and returns its sum with 3. We can call add3 with any integer argument to get the sum with 3:

Kotlin
val result = add3(4) // result is 7

Partial application can be used to create more specialized functions from more general functions, without duplicating code. It can also be used to simplify complex functions by breaking them down into smaller, more manageable functions.

One of the benefits of partial application is that it allows for more flexible and composable code. For example, suppose we have a function power that takes a base and an exponent and returns the result of raising the base to the exponent:

Kotlin
fun power(base: Double, exponent: Double): Double {
    return Math.pow(base, exponent)
}

We can use partial application to create new functions that calculate the square, cube, or any other power of a number without duplicating code. For example, we can define a function square that calculates the square of a number by partially applying power with an exponent of 2:

Kotlin
val square = { x: Double -> power(x, 2.0) }

Now square is a new function that takes a single argument and returns its square. We can call square with any double argument to get the square:

Kotlin
val result = square(3.0) // result is 9.0

Similarly, we can define a function cube that calculates the cube of a number by partially applying power with an exponent of 3:

Kotlin
val cube = { x: Double -> power(x, 3.0) }

Now cube is a new function that takes a single argument and returns its cube. We can call cube with any double argument to get the cube:

Kotlin
val result = cube(2.0) // result is 8.0

Partial application can also be used to create more specialized functions from more general functions, without duplicating code. For example, suppose we have a function sum that takes a list of integers and returns their sum:

Kotlin
fun sum(numbers: List<Int>): Int {
    return numbers.sum()
}

We can use partial application to create a new function sumEven that calculates the sum of even numbers in a list by partially applying sum with a filter function that selects even numbers:

Kotlin
val sumEven = { numbers: List<Int> -> sum(numbers.filter { it % 2 == 0 }) }

Now sumEven is a new function that takes a list of integers and returns their sum, but only for the even numbers in the list. We can call sumEven with any list of integers to get the sum of even numbers:

Kotlin
val result = sumEven(listOf(1, 2, 3, 4, 5, 6)) // result is 12

Function composition

Function composition is related to currying in Kotlin in that both techniques are used to combine functions into more complex operations. Function composition involves taking two or more functions and combining them into a single function that performs both operations. Currying, on the other hand, involves taking a function that takes multiple arguments and transforming it into a series of functions that each take a single argument.

Function composition can be thought of as a special case of currying, where the input to each function is the output of the previous function. In other words, function composition is a form of currying where the arity of the functions being composed is limited to two functions.

In Kotlin, function composition and currying can be used together to create powerful and expressive code. By composing and currying functions, you can build up complex operations from simpler building blocks, making your code more modular and easier to read and maintain.

For example, you might have two functions, add and multiply, that take two arguments each:

Kotlin
fun add(x: Int, y: Int): Int {
    return x + y
}

fun multiply(x: Int, y: Int): Int {
    return x * y
}

You can use function composition to create a new function, addAndMultiply, that adds two numbers and then multiplies the result by a third number:

Kotlin
val addAndMultiply = { x: Int, y: Int, z: Int ->
    multiply(add(x, y), z)
}

Alternatively, you could use currying to transform the add and multiply functions into unary functions that each take a single argument:

Kotlin
val addCurried = { x: Int -> { y: Int -> add(x, y) } }
val multiplyCurried = { x: Int -> { y: Int -> multiply(x, y) } }

You can then use these curried functions to create a new function, addAndMultiplyCurried, that performs the same operation as the addAndMultiply function:

Kotlin
val addAndMultiplyCurried = { x: Int ->
    { y: Int ->
        { z: Int ->
            multiplyCurried(addCurried(x)(y))(z)
        }
    }
}

In both cases, you end up with a new function that performs a complex operation by combining simpler functions using either function composition or currying.

No currying

“No currying” simply means that a programming language does not have built-in support for currying. In other words, you cannot use currying directly in the language syntax, but you can still implement it manually using language features like closures or higher-order functions.

Kotlin, for example, does not have built-in support for currying, but you can still create curried functions using higher-order functions and closures. For instance, you can create a curried version of a two-argument function by defining a function that takes the first argument and returns another function that takes the second argument:

Kotlin
fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C {
    return { a: A -> { b: B -> f(a, b) } }
}

This function takes a two-argument function f and returns a curried version of f that takes the first argument and returns another function that takes the second argument. You can then use this curried function like this:

Kotlin
fun add(a: Int, b: Int): Int = a + b
Kotlin
val curriedAdd = curry(::add)
Kotlin
val add3 = curriedAdd(3)
val result = add3(4) // returns 7

In this example, curriedAdd is a curried version of the add function, which takes the first argument a and returns another function that takes the second argument b. You can then use curriedAdd to create a new function add3 that takes only one argument (a), and returns a function that adds a to 3. Finally, you can call add3 with the second argument 4 to get the result 7.

Uncurry in kotlin

In functional programming, uncurrying is the process of converting a curried function into a function that takes multiple arguments. In Kotlin, you can implement uncurrying manually using higher-order functions and lambdas.

For example, let’s say you have a curried function that takes two arguments and returns a result:

Kotlin
fun add(a: Int): (Int) -> Int = { b -> a + b }

This function takes an integer a and returns a lambda that takes another integer b and returns the sum of a and b.

To uncurry this function, you can define a higher-order function that takes a curried function and returns a function that takes multiple arguments. Here’s an example implementation:

Kotlin
fun <A, B, C> uncurry(f: (A) -> (B) -> C): (A, B) -> C {
    return { a: A, b: B -> f(a)(b) }
}

This uncurry function takes a curried function f and returns a new function that takes two arguments (a and b) and applies them to f to get the result. You can then use this function to uncurry the add function like this:

Kotlin
val uncurriedAdd = uncurry(::add)

val result = uncurriedAdd(3, 4) // returns 7

In this example, uncurriedAdd is the uncurried version of the add function, which takes two arguments a and b, and returns their sum. You can then call uncurriedAdd with the two arguments 3 and 4 to get the result 7.

Are ‘no currying’ and ‘uncurrying’ the same concept in Kotlin?

No, “no currying” and “uncurrying” are not the same concept in Kotlin. “No currying” simply means that Kotlin does not have built-in support for currying, meaning you cannot directly define a function that returns another function.

On the other hand, “uncurrying” is the process of converting a curried function into a function that takes multiple arguments. This can be done manually using higher-order functions and lambdas in Kotlin.

So, while “no currying” means that you cannot directly define a curried function in Kotlin, “uncurrying” is a way to convert a curried function into a non-curried function if you need to use it in that form.

Currying in the Kotlin Ecosystem

Currying is a technique that is commonly used in functional programming, which is a programming paradigm that is well-supported in the Kotlin ecosystem. As such, there are several libraries and frameworks in the Kotlin ecosystem that provide support for currying.

Here are a few examples:

  1. Arrow: Arrow is a functional programming library for Kotlin that provides support for many functional programming concepts, including currying. Arrow provides a curried function that can be used to curry any function in Kotlin.
  2. Kategory: Kategory is another functional programming library for Kotlin that provides support for currying. Kategory provides a curried function that can be used to curry any function in Kotlin, as well as several other utility functions for working with curried functions.
  3. Kotlin stdlib: The Kotlin standard library includes several functions that can be used to curry functions. For example, the fun <P1, P2, R> Function2<P1, P2, R>.curried(): (P1) -> (P2) -> R extension function can be used to curry a two-argument function.
  4. Koin: Koin is a popular dependency injection framework for Kotlin that supports currying. Koin provides a factory function that can be used to create a curried factory function that returns instances of a given type.

These are just a few examples of the many libraries and frameworks in the Kotlin ecosystem that support currying. With the increasing popularity of functional programming in Kotlin, it is likely that we will see even more support for currying in the future.

Advantages and Disadvantages

Here are some advantages and disadvantages of using currying in Kotlin:

Advantages:

  1. Increased modularity: Currying allows you to break down complex functions into smaller, more modular functions. This makes your code easier to read, understand, and maintain.
  2. Code reuse: By currying functions, you can create smaller, reusable functions that can be used in multiple contexts. This reduces code duplication and helps you write more concise and reusable code.
  3. Improved type safety: Currying can help improve type safety by ensuring that each curried function takes exactly one argument of the correct type. This can help catch errors at compile time and make your code more robust.
  4. Improved readability: By currying functions, you can create more readable code that clearly expresses the intent of the code. This can make your code easier to understand and maintain.

Disadvantages:

  1. Performance overhead: Currying involves creating new functions for each argument, which can lead to performance overhead. In some cases, the performance overhead of currying may outweigh the benefits of modularity and code reuse.
  2. Increased complexity: Currying can make code more complex, especially if you are not familiar with the technique. This can make it harder to debug and maintain your code.
  3. Less intuitive: Currying can be less intuitive than traditional function calls, especially if you are used to imperative programming. This can make it harder to understand and reason about your code.
  4. Potential for misuse: Currying can be a powerful technique, but it can also be misused. It is important to use currying judiciously and only when it makes sense for the specific use case.

Conclusion

In this blog post, we explored the concept of currying in Kotlin and how it can be used to write more concise and expressive code. We looked at several examples of curried functions, including adding numbers and filtering lists, to demonstrate how currying can simplify complex functions, promote reusability, and enable function composition. By leveraging the power of currying, Kotlin developers can write more modular, maintainable, and reusable code that is easier to test and debug.

Author

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!