The also
function in Kotlin serves a unique purpose in the family of scope functions. While other scope functions like let
and run
are primarily used for transformations, also
shines when you need to perform additional operations or side effects while maintaining a fluent method chain. In this comprehensive guide, we'll explore how also
works and when it's the right tool for the job.
What is the also Function?
The also
function executes a block of code on an object and returns the object itself. What makes it special is that it provides the object as a parameter (it
) while keeping the object as the return value, making it perfect for side effects in method chains.
Here's a simple example to illustrate its basic usage:
data class User(
var name: String,
var email: String
)
val user = User("John", "[email protected]")
.also {
println("Created user: ${it.name}") // Side effect: logging
}
.also {
saveToAuditLog(it) // Side effect: audit logging
}
Key Characteristics of also
The also
function has three main characteristics that distinguish it from other scope functions:
- Context Object: The object is available as a lambda parameter (typically
it
) - Return Value: Returns the object itself (the receiver object)
- Purpose: Designed for performing side effects without breaking the method chain
Let's see these characteristics in action with a more detailed example:
class UserRepository {
fun createUser(request: UserRequest): User {
return User(
name = request.name.trim(),
email = request.email.lowercase()
).also { newUser ->
// Validate the created user
require(newUser.email.contains("@")) {
"Invalid email format for user: ${newUser.name}"
}
}.also { validUser ->
// Log the creation
logger.info("Creating new user: ${validUser.name}")
}.also { userToSave ->
// Save to database
database.save(userToSave)
}
// The chain returns the User object itself
}
}
Common Use Cases for also
1. Debugging and Logging
One of the most practical uses of also
is adding debug points in your method chains:
class OrderProcessor {
fun processOrder(order: Order) = order
.validate()
.also { validOrder ->
logger.debug("Order validated: ${validOrder.id}")
}
.applyDiscounts()
.also { discountedOrder ->
logger.debug("Applied discounts, new total: ${discountedOrder.total}")
}
.process()
.also { processedOrder ->
logger.info("Successfully processed order: ${processedOrder.id}")
}
}
2. Additional Validation
Using also
for validation steps keeps the code clean and maintainable:
class DocumentProcessor {
fun processDocument(document: Document) = document
.parse()
.also { parsed ->
require(parsed.content.isNotBlank()) {
"Document cannot be empty"
}
}
.analyze()
.also { analyzed ->
require(analyzed.score > 0) {
"Document analysis failed with score: ${analyzed.score}"
}
}
.generateReport()
}
3. Object Initialization Side Effects
When initializing objects that require additional setup or registration:
class CacheManager {
private val cache = mutableMapOf<String, Any>()
fun <T : Any> createCacheEntry(key: String, value: T): T {
return value.also { newValue ->
cache[key] = newValue
notifyListeners(key, newValue)
updateMetrics(cache.size)
}
}
}
Best Practices
1. Use Meaningful Parameter Names
Instead of using the default it
, give the parameter a meaningful name when the block is complex:
data class Product(
val name: String,
var price: Double,
var inStock: Boolean
)
fun updateProduct(product: Product) = product
.also { updatingProduct ->
require(updatingProduct.price > 0) {
"Price must be positive"
}
}
.also { validProduct ->
notifyPriceUpdate(validProduct)
}
2. Keep Side Effects Pure
While also
is meant for side effects, these effects should be predictable and not modify the object unexpectedly:
// Good: Side effect is pure and predictable
user.also {
logger.info("Processing user: ${it.name}")
}
// Avoid: Unexpected modification
user.also {
it.name = it.name.uppercase() // Surprising side effect
}
3. Chain Responsibly
While also
is great for method chaining, don't overuse it:
// Good: Clear, focused chain
document
.parse()
.also { logParsingComplete(it) }
.process()
.also { logProcessingComplete(it) }
// Avoid: Too many operations
document.also {
logStart(it)
validateDocument(it)
updateMetrics(it)
notifyUsers(it)
// Too many responsibilities
}
Comparing also with Other Scope Functions
also vs let
// also: Performs side effects, returns original object
user.also {
logger.info("Processing: ${it.name}")
} // Returns user
// let: Transforms object, returns new result
user.let {
UserDTO(it.name, it.email)
} // Returns UserDTO
also vs apply
// also: Uses 'it', good for side effects
user.also {
println("User name is: ${it.name}")
}
// apply: Uses 'this', good for configuration
user.apply {
name = "John"
email = "[email protected]"
}
also vs run
// also: Returns original object
val user = User().also {
validateUser(it)
} // Returns User
// run: Returns last expression
val isValid = user.run {
validateUser(this)
} // Returns Boolean
Common Pitfalls
1. Modifying the Object
While it's possible to modify the object within also
, it's generally not recommended:
// Avoid this
user.also {
it.name = it.name.uppercase() // Modification
}.also {
it.email = it.email.lowercase() // Another modification
}
// Better approach
user.apply {
name = name.uppercase()
email = email.lowercase()
}
Don't use also
when you actually need to transform the object:
// Incorrect usage
val userDTO = user.also {
UserDTO(it.name, it.email) // This transformation is ignored!
}
// Correct approach
val userDTO = user.let {
UserDTO(it.name, it.email)
}
Conclusion
The also
function is a powerful tool in Kotlin's scope function family, specifically designed for performing side effects while maintaining clean method chains. It's particularly valuable for logging, validation, and other operations that shouldn't break the flow of your code.
Key points to remember about also
:
- Use it for side effects, not transformations
- It returns the original object, making it perfect for chaining
- Keep side effects pure and predictable
- Use meaningful parameter names for clarity
- Don't overuse it in chains
By understanding these characteristics and following the best practices, you can write more maintainable and readable code that clearly separates side effects from your main business logic.