Kotlin Currying
Introduction
Currying is an important functional programming technique named after mathematician Haskell Curry. It's the process of transforming a function that takes multiple arguments into a sequence of functions that each take a single argument. While Kotlin doesn't support currying directly in its syntax like some pure functional languages (such as Haskell), we can implement it manually using Kotlin's powerful higher-order function capabilities.
In this tutorial, we'll explore how currying works in Kotlin, why it's useful, and how you can apply it in your own code to create more flexible, composable functions.
Understanding Currying
What is Currying?
In regular function calls, you provide all arguments at once:
// Regular function with two parameters
fun add(a: Int, b: Int): Int = a + b
// Called with both arguments at once
val result = add(5, 3) // 8
With currying, you transform this function into a chain of functions, each taking one argument:
// Curried version
fun addCurried(a: Int): (Int) -> Int {
return { b -> a + b }
}
// Usage:
val add5 = addCurried(5) // Returns a function that adds 5 to its argument
val result = add5(3) // 8
In this example, addCurried
takes one argument and returns a function that takes another argument.
Creating Curried Functions in Kotlin
Manual Currying
Let's start by manually currying a simple function with multiple parameters:
// Original function with 3 parameters
fun volume(length: Int, width: Int, height: Int): Int = length * width * height
// Manually curried version
fun curriedVolume(length: Int): (Int) -> (Int) -> Int {
return { width ->
{ height ->
length * width * height
}
}
}
// Usage example
fun main() {
// Regular function call
val cubeVolume = volume(2, 3, 4)
println("Regular function result: $cubeVolume") // 24
// Curried function calls
val lengthFixed = curriedVolume(2)
val lengthAndWidthFixed = lengthFixed(3)
val result = lengthAndWidthFixed(4)
println("Curried step by step: $result") // 24
// Or in a single chain
val chainedResult = curriedVolume(2)(3)(4)
println("Curried chained result: $chainedResult") // 24
}
Creating Currying Extensions
We can create extension functions to curry any function automatically:
// Extension function to curry a function with 2 parameters
fun <A, B, R> ((A, B) -> R).curry(): (A) -> (B) -> R {
return { a -> { b -> this(a, b) } }
}
// Extension function to curry a function with 3 parameters
fun <A, B, C, R> ((A, B, C) -> R).curry(): (A) -> (B) -> (C) -> R {
return { a -> { b -> { c -> this(a, b, c) } } }
}
// Usage examples
fun main() {
val add: (Int, Int) -> Int = { a, b -> a + b }
val curriedAdd = add.curry()
val add5 = curriedAdd(5)
println(add5(10)) // 15
val multiply: (Int, Int, Int) -> Int = { a, b, c -> a * b * c }
val curriedMultiply = multiply.curry()
val double = curriedMultiply(2)
val doubleAndTriple = double(3)
println(doubleAndTriple(4)) // 24
}
Benefits of Currying
1. Function Specialization
Currying allows you to create specialized versions of more general functions:
// A general greeting function
val greet: (String, String) -> String = { greeting, name -> "$greeting, $name!" }
val curriedGreet = greet.curry()
// Create specialized greeting functions
val sayHello = curriedGreet("Hello")
val sayHi = curriedGreet("Hi")
fun main() {
println(sayHello("John")) // "Hello, John!"
println(sayHi("Sarah")) // "Hi, Sarah!"
}
2. Partial Application
Currying enables partial application, where you fix some parameters of a function and leave others to be filled in later:
// A function to format messages with different attributes
fun formatMessage(prefix: String, text: String, suffix: String): String {
return "$prefix - $text - $suffix"
}
val curriedFormat = { prefix: String ->
{ text: String ->
{ suffix: String ->
formatMessage(prefix, text, suffix)
}
}
}
fun main() {
// Create specialized formatters
val errorFormatter = curriedFormat("ERROR")
val warningMessage = errorFormatter("Disk space low")("HIGH_PRIORITY")
println(warningMessage) // "ERROR - Disk space low - HIGH_PRIORITY"
// Create a formatter that always uses the same prefix and suffix
val logFormatter = curriedFormat("LOG")
val logWithPriority = { message: String -> logFormatter(message)("INFO") }
println(logWithPriority("System started")) // "LOG - System started - INFO"
}
Real-World Applications
Configuration Functions
Currying is particularly useful for configuration functions:
// A database connection function with many parameters
fun connectToDatabase(
host: String,
port: Int,
username: String,
password: String,
database: String,
ssl: Boolean
): DatabaseConnection {
println("Connecting to $database on $host:$port")
// In a real application, this would return an actual connection
return DatabaseConnection("$host:$port/$database")
}
// Curried version
val curriedConnect = { host: String ->
{ port: Int ->
{ username: String ->
{ password: String ->
{ database: String ->
{ ssl: Boolean ->
connectToDatabase(host, port, username, password, database, ssl)
}
}
}
}
}
}
// Mock class for the example
class DatabaseConnection(val connectionString: String)
fun main() {
// Create a partially applied function for a specific server
val connectToLocalhost = curriedConnect("localhost")(5432)
// Further specialize with a specific user
val connectAsAdmin = connectToLocalhost("admin")("admin123")
// Now we can connect to different databases easily
val usersDb = connectAsAdmin("users")(true)
val productsDb = connectAsAdmin("products")(true)
println(usersDb.connectionString) // localhost:5432/users
println(productsDb.connectionString) // localhost:5432/products
}
Event Handling
Currying can be useful for handling events in a functional style:
// Define an event handler type
typealias EventHandler<T> = (T) -> Unit
// Event system that can have multiple handlers for different event types
class EventSystem {
private val handlers = mutableMapOf<String, MutableList<EventHandler<Any>>>()
fun <T> addHandler(eventType: String, handler: EventHandler<T>) {
@Suppress("UNCHECKED_CAST")
handlers.getOrPut(eventType) { mutableListOf() }
.add(handler as EventHandler<Any>)
}
fun <T> emit(eventType: String, data: T) {
handlers[eventType]?.forEach { it(data) }
}
}
// A curried function to create event handlers
fun <T> createHandler(prefix: String): (Boolean) -> (T) -> Unit {
return { showTimestamp ->
{ data ->
val timestamp = if (showTimestamp) "[${System.currentTimeMillis()}] " else ""
println("$timestamp$prefix: $data")
}
}
}
fun main() {
val eventSystem = EventSystem()
// Create specialized handlers using currying
val errorHandler = createHandler<String>("ERROR")(true)
val debugHandler = createHandler<String>("DEBUG")(false)
// Register handlers
eventSystem.addHandler("error", errorHandler)
eventSystem.addHandler("debug", debugHandler)
// Emit events
eventSystem.emit("error", "Failed to connect to database")
eventSystem.emit("debug", "Attempting to reconnect")
// Output will be something like:
// [1632386724578] ERROR: Failed to connect to database
// DEBUG: Attempting to reconnect
}
Currying vs. Regular Functions
Advantages of Currying
- Function Reusability: Create specialized functions from general ones
- Partial Application: Fix some arguments and apply others later
- Composition: Easier to compose with other functions
- Readability: Can make code more readable in certain scenarios
Disadvantages
- Syntax Overhead: More verbose than regular function calls in Kotlin
- Performance: May create more objects (closures) than direct calls
- Learning Curve: Less intuitive for developers not familiar with functional programming
Summary
Currying is a powerful functional programming technique that transforms a function that takes multiple arguments into a sequence of single-argument functions. In Kotlin, while not built directly into the language, we can implement currying using higher-order functions and lambdas.
Key takeaways:
- Currying transforms
f(a, b, c)
intof(a)(b)(c)
- It enables partial application - fixing some parameters while leaving others to be specified later
- It helps create specialized functions from more general ones
- Use extension functions to curry existing functions
- Best used for configuration, event handling, and building composable APIs
Currying is just one of many functional programming techniques that can help make your Kotlin code more flexible and composable. As you become more comfortable with the concept, you'll discover many situations where currying provides elegant solutions to complex problems.
Exercises
- Write a curried function for calculating the total price with tax and discount:
totalPrice(basePrice)(taxRate)(discountRate)
- Create a currying extension function for a 4-parameter function
- Implement a curry function that works with any function with up to 5 parameters
- Create a real-world example using currying for a logging system with different severity levels and output formats
- Refactor an existing function in your codebase to use currying and evaluate the benefits
Additional Resources
- Kotlin Official Documentation on Higher-Order Functions
- "Functional Programming in Kotlin" by Marco Vermeulen, Rúnar Bjarnason, and Paul Chiusano
- Arrow Kt Library - A functional companion library for Kotlin that includes currying utilities
Happy currying!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)