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:
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:
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:
- 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.
- 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.
- 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
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
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
fun add(a: Int, b: Int): Int = a + b
val curriedAdd = curry(::add)
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:
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:
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:
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:
- 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. - 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. - 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. - 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:
- 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.
- 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.
- 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.
- 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:
- 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.
- 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.
- 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.
- 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.