Kotlin Partial Application
Introduction
In the world of functional programming, partial application is a powerful technique that allows you to transform a function that takes multiple arguments into a function that takes fewer arguments. This is done by "fixing" or "pre-filling" some of the original function's parameters, resulting in a new function that only requires the remaining parameters.
Partial application is closely related to, but distinct from, currying (another functional programming concept). While currying transforms a function with multiple parameters into a sequence of functions that each take a single parameter, partial application creates a new function by supplying some of the parameters to an existing function.
In this tutorial, we'll explore how to implement and use partial application in Kotlin and understand its practical benefits.
Understanding Partial Application
Let's start with a simple example. Consider a function that adds two numbers:
fun add(a: Int, b: Int): Int = a + b
With partial application, we can create a new function by fixing the first parameter:
fun addFive(b: Int): Int = add(5, b)
Now, addFive
is a partial application of add
where the first parameter is fixed to 5
.
Implementing Partial Application in Kotlin
Using Higher-Order Functions
Kotlin's support for higher-order functions makes it straightforward to implement partial application:
fun add(a: Int, b: Int): Int = a + b
// Creating a partial application of add
fun partialAdd(a: Int): (Int) -> Int {
return { b -> add(a, b) }
}
fun main() {
val addFive = partialAdd(5)
println(addFive(3)) // Output: 8
println(addFive(7)) // Output: 12
}
Using Function References and Extension Functions
We can make partial application more generic using extension functions on function types:
// Extension function for partial application on a two-parameter function
fun <A, B, R> ((A, B) -> R).partial(a: A): (B) -> R {
return { b -> this(a, b) }
}
fun main() {
val add: (Int, Int) -> Int = { a, b -> a + b }
val addTen = add.partial(10)
println(addTen(5)) // Output: 15
println(addTen(20)) // Output: 30
}
Three-Parameter Function Example
Let's see how partial application works with more parameters:
fun calculate(a: Int, b: Int, c: Int): Int = a * b + c
// Extension function for partial application on three-parameter functions
fun <A, B, C, R> ((A, B, C) -> R).partial(a: A): (B, C) -> R {
return { b, c -> this(a, b, c) }
}
// Extension function for partial application with two fixed parameters
fun <A, B, C, R> ((A, B, C) -> R).partial(a: A, b: B): (C) -> R {
return { c -> this(a, b, c) }
}
fun main() {
val calculate: (Int, Int, Int) -> Int = { a, b, c -> a * b + c }
val multiplyByTwoAndAdd = calculate.partial(2)
println(multiplyByTwoAndAdd(5, 3)) // Output: 2*5+3 = 13
val multiplyFiveAddSeven = calculate.partial(5, 7)
println(multiplyFiveAddSeven(3)) // Output: 5*7+3 = 38
}
Practical Applications of Partial Application
1. Configuration and Default Parameters
Partial application can be used to configure functions with default settings:
data class User(val id: Int, val name: String, val role: String)
fun createUser(id: Int, name: String, role: String): User = User(id, name, role)
fun main() {
// Create functions for creating users with predefined roles
val createAdmin = { id: Int, name: String -> createUser(id, name, "ADMIN") }
val createModerator = { id: Int, name: String -> createUser(id, name, "MODERATOR") }
val admin = createAdmin(1, "Alice")
val moderator = createModerator(2, "Bob")
println(admin) // Output: User(id=1, name=Alice, role=ADMIN)
println(moderator) // Output: User(id=2, name=Bob, role=MODERATOR)
}
2. API Request Construction
Partial application is useful when working with APIs that require multiple parameters, some of which remain constant across requests:
data class ApiRequest(val url: String, val method: String, val headers: Map<String, String>, val body: String?)
fun makeRequest(url: String, method: String, headers: Map<String, String>, body: String?): ApiRequest {
return ApiRequest(url, method, headers, body)
}
fun main() {
val baseHeaders = mapOf("Content-Type" to "application/json", "Authorization" to "Bearer token123")
// Create partially applied functions for different API endpoints
val makeGetRequest: (String) -> ApiRequest = { url ->
makeRequest(url, "GET", baseHeaders, null)
}
val makePostRequest: (String, String) -> ApiRequest = { url, body ->
makeRequest(url, "POST", baseHeaders, body)
}
val getUsersRequest = makeGetRequest("https://api.example.com/users")
val createUserRequest = makePostRequest("https://api.example.com/users", """{"name":"John","email":"[email protected]"}""")
println(getUsersRequest)
// Output: ApiRequest(url=https://api.example.com/users, method=GET, headers={Content-Type=application/json, Authorization=Bearer token123}, body=null)
println(createUserRequest)
// Output: ApiRequest(url=https://api.example.com/users, method=POST, headers={Content-Type=application/json, Authorization=Bearer token123}, body={"name":"John","email":"[email protected]"})
}
3. Event Handlers and Callbacks
Partial application is particularly useful when working with event handlers that require context:
data class Event(val id: String, val type: String, val data: Map<String, Any>)
fun processEvent(logger: (String) -> Unit, eventType: String, event: Event) {
logger("Processing $eventType event: ${event.id}")
// Process the event based on its type
when (eventType) {
"CLICK" -> logger("User clicked on ${event.data["element"]}")
"SUBMIT" -> logger("Form submitted with data: ${event.data["formData"]}")
else -> logger("Unknown event type")
}
}
fun main() {
val logger = { message: String -> println("[${System.currentTimeMillis()}] $message") }
// Create specialized event processors using partial application
val processClickEvent: (Event) -> Unit = { event -> processEvent(logger, "CLICK", event) }
val processSubmitEvent: (Event) -> Unit = { event -> processEvent(logger, "SUBMIT", event) }
val clickEvent = Event("e123", "CLICK", mapOf("element" to "submit-button"))
val submitEvent = Event("e456", "SUBMIT", mapOf("formData" to "username=john"))
processClickEvent(clickEvent)
// Output: [timestamp] Processing CLICK event: e123
// [timestamp] User clicked on submit-button
processSubmitEvent(submitEvent)
// Output: [timestamp] Processing SUBMIT event: e456
// [timestamp] Form submitted with data: username=john
}
Comparison with Currying
While partial application and currying are related, they are not the same:
// Currying: Transform a function with multiple parameters into a sequence of functions
fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C {
return { a -> { b -> f(a, b) } }
}
// Partial application: Fix some parameters of a function
fun <A, B, C> ((A, B) -> C).partiallyApply(a: A): (B) -> C {
return { b -> this(a, b) }
}
fun main() {
val add: (Int, Int) -> Int = { a, b -> a + b }
// Currying
val curriedAdd = curry(add)
val addFive1 = curriedAdd(5)
println(addFive1(3)) // Output: 8
// Partial application
val addFive2 = add.partiallyApply(5)
println(addFive2(3)) // Output: 8
}
The main differences are:
- Currying transforms a function into a chain of single-parameter functions
- Partial application creates a new function by fixing some parameters of an existing function
- Both achieve similar results but with different structures and applications
Summary
Partial application is a powerful functional programming technique that allows you to create specialized functions from more general ones by fixing some of their parameters. In Kotlin, this can be implemented using:
- Higher-order functions
- Extension functions on function types
- Lambda expressions with closures
The benefits of partial application include:
- Creating more specialized, reusable functions
- Simplifying complex function calls
- Making code more readable and maintainable
- Enabling better separation of concerns
By mastering partial application, you can write more concise and expressive code that follows functional programming principles.
Exercises
- Write a function that uses partial application to create a greeter function that includes the current time.
- Implement a generic
partial
function that works with functions of any arity. - Create a logging system that uses partial application to create specialized loggers for different components of your application.
- Compare the performance of partial application versus using default parameters in Kotlin.
- Try combining partial application with other functional programming techniques like composition and memoization.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)