Functional composition, that is, chaining simple functions into a more complex one, is a common practice in Kotlin. However, in doing this, you can end up with deeply nested code that makes it less readable and harder to maintain.
Consider an example where you are calculating a product’s final price by applying a discount and adding shipping costs and tax. This can result in multiple nested function calls:
val finalPrice = addTax(
addShipping(
applyDiscount(
getProduct(123)
)
)
)
While this structure works, it is difficult to follow or reason about. The let
function simplifies functional composition, allowing functions to be called in the correct sequence and thus easier to read.
In functional programming, functions are first-class citizens, meaning they can be passed, returned, and combined to form complex operations. Functional composition — chaining simple functions to create readable and testable workflows — is a key concept. As Kotlin Crash Course, Chapter 8: Functional programming explains, this style makes code modular and easier to maintain.
In this article, you will learn how let
can simplify functional composition in Kotlin and when to consider other scope functions.
Scope functions such as let
allow you to execute a piece of code in the context of an object, making code more clear and concise. The following is an overview of how let
works and its use cases:
Purpose: let
is primarily used to apply a transformation to an object or perform additional operations without modifying the object itself.
Syntax: let
takes a lambda with a context object (it
), and returns the lambda’s result, allowing for further operations to be chained.
Typical use cases:
- Data transformation: You can use the
let
function to transform data without changing the original object. - Safe calls on nullable objects: If you have a nullable object and want to make a safe call, you can put the piece of code inside a let function and thus run it only if the context object is not null.
- Isolating logic:
let
can help you to visually isolate a chunk of logic that is associated with the context object, thus grouping it in a readable, maintainable way.
Suppose you want to display a product’s details only if the product is not null:
val product = getProduct(123)
product?.let {
val finalDetails = formatProductDetails(it)
println(finalDetails)
}
In the above example, let
is used to print product details if product
is not null.
When performing more complex operations, using let
can significantly improve the structure and clarity of your code. It permits not just the application of a function to the result of an expression but also taking that result and using it in subsequent expressions. The clean, cascading use of let
allows expressions to flow one into another as if they were a series of dominoes falling in sequence. You can see this in action when considering a fairly typical and straightforward series of operations.
Let us go back to the scenario where you have to:
- Fetch a product
- Apply a discount to product’s price
- Add shipping costs
- And finally add tax
Without let
(Nested Functional Composition):
val finalPrice = addTax(
addShipping(
applyDiscount(
getProduct(123)
)
)
)
This nested structure works, but because each function is embedded within one another, the flow is harder to follow.
With let
(Readable Functional Composition):
val finalPrice = getProduct(123)
.let(::applyDiscount)
.let(::addShipping)
.let(::addTax)
This version shows a considerable improvement in readability. Using let
in this way makes each transformation within functional composition explicit and sequential.
While let
is excellent for chaining transformations, there are cases in functional composition where other scope functions may be more suitable, for instance when modifying properties of the same object within a chain:
- Use
apply
for Modifying Properties: If one of the functions in your chain needs to modify properties directly on the context object itself,apply
is a better choice. - Use
also
for Additional Actions: Usealso
to perform side-effect operations, like printing the final price, without altering the flow. - Combine scope functions in Mixed Chains: Combine scope functions for more complex chains, such as using
let
for transformations andapply
for configuration.
Understanding when to use each scope function ensures your functional compositions in Kotlin are both clear and concise.
In Kotlin, the let
scope function can simplify functional composition and enhance code readability. By understanding when to use let
and how to structure transformation chains, you can write cleaner, more expressive code that aligns with functional programming principles.
For hands-on, in-depth guidance on when and how to use scope functions like let
, apply
, also
, run
, and with
, see Chapter 7 of Kotlin Crash Course.
- Use
let
to Simplify Functional Composition:let
is a great choice for chaining transformations in a readable way. By isolating each step, you create a clear flow that is easier to follow. - Avoid
let
for Property Modifications: When the goal is to modify properties of an object, consider usingapply
instead, as it is designed for object property modifications.
For more Kotlin tips and tricks, visit my Medium blog homepage.