Build function chains using composition in Kotlin
Use aggregation to build function chains
We are going to focus on how we can use functions to alter the nature of an object and change it, and how we can chain functions together to apply their effects on an object in one go with a composite pattern.
By the end of this article, we would cover:
- What is function chaining?
- Understand how Jetpack Compose Modifiers work
- Learn aggregation using fold functions
- Build our version of Compose() builder with function chaining
Table of content
- Table of content
- Function chaining
- What is aggregation?
- Essentials of Compose UI modifiers
- Visualizing compose modifier function chain
- Visualizing view nesting based on fold direction
- Building our function chaining with Composition
- Building an object with our Compose() function
- Extending and using the Chain
- The result
- Summary
Function chaining
I’m sure with the recent stable release of Jetpack Compose, you are familiar with its Modifier system. The modifier system by class design allows us to set “modifications” on the class by chaining functions.
// f(x) -> g(x) -> h(x)
Modifier.width(10.dp).height(10.dp).padding(start = 8.dp).background(color = Color(0xFFFFF0C4))
Notice how each function can be called directly after the invocation of the other, once .width() is called immediately you can call .height() and in this manner, endlessly chain the functions one after the other till the desired modifications are applied or composed together. Super convenient!
How can we build a pattern similar to this?
Let’s take an example of a Car class. It has two properties, ownerName and color.
class Car(var ownerName: String, var color: String) {
fun changeOwner(newName: String) {
this.ownerName = newName
}
fun repaint(newColor: String) {
this.color = newColor
}
}
If we create an instance of this class and try to access the functions, we can’t chain them successively.
var myCar: Car = Car("Sid","Red")
// Standard way of calling functions one after another
myCar.changeOwner("New Owner")
myCar.repaint("Blue")
// Not possible, none of the functions can be chained together
myCar.changeOwner("Sid Patil").repaint("Green")
The first thing we all would probably think about is just returning the instance of the same class and chaining will be possible. But this means changing all functions to return an instance of the same class with the return type specifier.
// Now both functions return the instance of the class and have return type specifiers
fun changeOwner(newName: String) : Car {
this.ownerName = newName
return this
}
fun repaint(newColor: String) : Car {
this.color = newColor
return this
}
The above works but does not support multiple types and is not intuitive. The compose modifier system has separate typed modifiers eg PaddingModifier, FillModifier, etc and they combine to form linkages between modifiers and generate views.
At this point let me highlight that the compose Modifier system leverages Kotlin’s extension function capabilities. For example, take fillMaxWidth()
fun Modifier.fillMaxWidth(fraction: Float = 1f) : Modifier
Here the function is extending the Modifier object. The function also returns a Modifier instance so that consecutive function calls can be chained together.
Wait.. but how is this any different than us simply returning class instances to enable function chaining?
Firstly it supports multiple types of classes (Since modifier is an interface), different implementations of Modifiers can be returned and the chain would still work. Secondly, the modifier system uses an aggregation logic to enable chaining.
What is aggregation?
Aggregation: a cluster of things that have come or been brought together
In our context, aggregation applies to the act of collectively applying operations together. This means applying our functions in a chain to the instance of the object and altering its nature. Kotlin contains powerful in-built aggregating functions.
Folding
Kotlin’s fold() function allows us to aggregate operations and collects the result once all operations are performed. Here in our example, we sum all numbers from 1 to 5 using fold.
// starts folding with initial value 0
// aggregates operation from left to right
val numbers = listOf(1,2,3,4,5)
numbers.fold(0) { total, number -> total + number}
There is reduce() function as well, which we can also use if we don’t want an initial value before aggregation. Folding is always done in a given direction, left to right or right to left.
val numbers = listOf(1,2,3,4,5)
// 1 + 2 -> 3 + 3 -> 6 + 4 -> 10 + 5 = 15
numbers.fold(0) { total, number -> total + number}
// 5 + 4 -> 9 + 3 -> 12 + 2 -> 14 + 1 = 15
numbers.foldRight(0) {total, number -> total + number}
The direction of the fold will determine the final result so it’s super important that we decide on a direction. In our example the operation was the same i.e. summation, if the operation were to be complex and dependent on the previous operations then your fold direction will determine your final result.
This is precisely why the sequence or order of your function calls on Modifier is important in deciding how Compose will render your UI.
Now that we’ve understood how aggregation works, let’s look in detail at how compose modifiers work.
Essentials of Compose UI modifiers
There are four essential components in compose modifiers
- Modifier Interface
- Modifier Element
- Modifier Companion
- Combined Modifier
An understanding of these four building blocks of Compose UI will allow us to gain insight into how we can build a simpler version of the same pattern.
I. Modifier Interface
The modifier interface is the starting unit of the modifier logic. Being an interface it outlines the various operations that can be performed by its implementing classes.
interface Modifier {
fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
fun any(predicate: (Element) -> Boolean): Boolean
fun all(predicate: (Element) -> Boolean): Boolean
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
}
The foldIn() function is essentially a fold() function moving from the left to right direction as we understood in the aggregation section. The foldOut() is opposite and moves from right to left applying its operations in the chain.
The then() function is what allows us to chain our operations, taking in a Modifier and returning a CombinedModifier. The first referential equality check just asserts that you don’t end up chaining your object and in case you do, returns the instance of the self.
The other two functions, any() and all() are used while materializing for the runtime. Materialize function is an extension of Composer which will prepare the modifiers for attaching to the runtime tree-node or for tooling in Android Studio through the layout inspector.
II. Modifier Element
The modifier element is a node within the modifier chain, think of this as a linkage. Many linkages together form the full chain data structure. The modifier element is also an interface.
interface Element : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R =
operation(this, initial)
override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this)
override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this)
}
III. Modifier Companion
The modifier companion is an object primarily responsible for starting the chain, it’s a simple companion object that implements the modifier interface.
companion object : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
override fun any(predicate: (Element) -> Boolean): Boolean = false
override fun all(predicate: (Element) -> Boolean): Boolean = true
override infix fun then(other: Modifier): Modifier = other
override fun toString() = "Modifier"
}
This companion is what is being called when you start chaining with Modifier.width().height()…
IV. Combined Modifier
The combined modifier forms as a unit of the chain and constitutes an Outer and Inner linkage, connecting Element and other CombinedModifier instances. This is a separate class, that implements the modifier interface as well.
class CombinedModifier(
private val outer: Modifier,
private val inner: Modifier
) : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
inner.foldIn(outer.foldIn(initial, operation), operation)
override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
outer.foldOut(inner.foldOut(initial, operation), operation)
override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
outer.any(predicate) || inner.any(predicate)
override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
outer.all(predicate) && inner.all(predicate)
override fun equals(other: Any?): Boolean =
other is CombinedModifier && outer == other.outer && inner == other.inner
override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()
override fun toString() = "[" + foldIn("") { acc, element ->
if (acc.isEmpty()) element.toString() else "$acc, $element"
} + "]"
}
Even though we are using the term chain as a data structure for organizing our nodes of modifiers, the right way to look at this is to understand that there is an outer chain node that wraps around another chain node. Each node of the chain is essentially a CombinedModifier forming linkage.
Visualizing compose modifier function chain
The best way to visualize the chain is keeping in mind the outer and inner object wrappings and the linkages formed due to this wrapping.
Modifier function chain
Visualizing view nesting based on fold direction
Another way to look at the chained modifiers is by visualizing how view modifiers are applied when you compose. The direction of composition is from the outermost view to the innermost view.
Nested views
The wrapping of objects within combined objects and linking them together through this wrapping itself while enabling traversing through the links using aggregating fold functions is the simple genius behind compose modifier logic.
We also saw that there is a foldOut() function that can also enable aggregation from the inner object to the outer object in the reverse direction if needed.
Building our function chaining with Composition
Phew! Now that we have covered some basics of function chaining, specifically regarding Compose and its modifiers, let us look into how we can build the same pattern for our projects in a simpler fashion along with compositional design.
I. Car Example
- Build a pattern that allows us to “Compose” objects
- The pattern must be extensible and reusable throughout our project
- Follow compositional design pattern
As an example for demonstration, We’ll build a Car class over which several properties will be composed.
Our car will have:
- Engine
- Seats
- Wheels
- Visuals
- Manufacturer
- Owner
II. Building the chain pattern
Taking inspiration from Compose, To build our project-wide chaining logic let’s start with a simple Chain interface
/**
* Chain core interface
*/
interface Chain {
/**
* Fold aggregator
* Direction: Left -> Right
* @param initial the inital object
* @param operation the operation to perform
* @return the type-specified object
*/
fun <R> fold(initial: R, operation: (R, ChainUnit) -> R): R
/**
* Form LinkedChain object linking two ChainUnits
* @return [LinkedChain] object
*/
fun then(other: Chain): Chain = LinkedChain(this, other)
/**
* A chain-unit
* Building block of our chain
*/
interface ChainUnit : Chain {
/**
* Fold aggregator
* Direction: Left -> Right
* @param initial the inital object
* @param operation the operation to perform
* @return the type-specified object
*/
override fun <R> fold(initial: R, operation: (R, ChainUnit) -> R): R =
operation(initial, this)
}
/**
* Companion builder for starting chains
*/
companion object : Chain {
override fun <R> fold(initial: R, operation: (R, ChainUnit) -> R): R = initial
override fun then(next: Chain): Chain = next
}
}
III. Components of the Chain
- Chain interface: Outlines chaining capabilities of folding and linking
- Chain Unit: The basic unit of our chain implementing capabilities of Chain interface
- Chain Companion: To start the chain
Our core interface contains one fold() function which moves from left to right and a then function that builds a LinkedChain. The then() function is what creates a LinkedChain. A companion is declared to start the Chain with initial implementations.
IV. LinkedChain
A LinkedChain houses the outer and inner objects and enables repeated nested wrapping to form the actual chain.
class LinkedChain(
private val outer: Chain,
private val inner: Chain
) : Chain {
override fun <R> fold(initial: R, operation: (R, Chain.ChainUnit) -> R): R =
inner.fold(outer.fold(initial, operation), operation)
}
We override the fold() function of the Chain interface and call the fold() function on the inner object passing the result of the fold() function on the outer object as a param to the inner fold() function.
(Fold-ception!)
Now that our Chain logic is complete let’s build our Car. Vroom!
Building an object with our Compose() function
We’ll build our car keeping in mind that we want to follow a compositional pattern and ensure that our properties of the car are built in a modular, plug-and-play manner allowing us to easily couple change or decouple them in the future if needed.
interface Vehicle {
fun isSafeForEnvironment() : Boolean
fun color() : String
fun seatCapacity() : Int
fun ownerDetails() : String
fun wheels() : Int
fun manufacturer() : String
}
We start by defining the functionality of our Car, here a Vehicle interface is used which will be implemented by our car class.
class Car private constructor(chainer: Chain) : Vehicle {
companion object {
fun compose(chainer: Chain) : Car {
return chainer.fold(Car(chainer)) { car, chainUnit ->
car
}
}
}
override fun isSafeForEnvironment(): Boolean {}
override fun color(): String {}
override fun seatCapacity(): Int {}
override fun ownerDetails(): String {}
override fun wheels(): Int {}
override fun manufacturer(): String {}
}
Our Car class implements the Vehicle interface, all the functions are not implemented yet (we’ll fix that). Notice that
- The constructor is private. It needs a Chain to build itself.
- We have a companion object in which we have a compose() function, which takes the chain as a param. The compose() function calls the fold() function to compose our car. For now, it’s just an empty function that just returns the Car through its type specifier and the accumulated result of the folding.
We want all the functionalities and properties of our Car to be modular, we’ll start by building four modules for the car; CarEngine, CarSpecs, CarOwner, and CarVisuals. Each module we build will implement ChainUnit interface for chaining capabilities.
class CarEngine(var engineType: EngineType) : Chain.ChainUnit {
fun needRecharge() : Boolean {
return (engineType == EngineType.ELECTRIC)
}
fun causesPollution() : Boolean {
return engineType == EngineType.DIESEL || engineType == EngineType.PETROL
}
}
class CarVisuals(var color: String) : Chain.ChainUnit {
fun isColorMetallic() : Boolean {
// some color determining logic here
// dummy response
return true
}
}
class CarSpecs(var wheelCount : Int, var seats: Int, var manufacturer: String) : Chain.ChainUnit {
// more functions here as needed
}
class CarOwner(var name: String, var licenceNo: String, var address: String) : Chain.ChainUnit {
fun isLicenceExpired() : Boolean {
// Some logic based on licenceNo
// dummy response
return false
}
}
For demonstration, I’ve implemented functions for the CarEngine module. The rest of the modules serve as an example for now but can house more logic.
Let’s add the modules to our car class and complete our compose() function. In the compose function, with each fold, we check if the chain unit is a given module and bind it to our object.
Note that in our example, during fold operations we are just binding modules, more powerful logic can be written per fold depending on ChainUnit type that will determine the final properties of our object just from the compose() function alone.
class Car private constructor(chainer: Chain) : Vehicle {
private var carEngine: CarEngine? = null
private var carOwner: CarOwner? = null
private var carSpecs: CarSpecs? = null
private var carVisuals: CarVisuals? = null
companion object {
fun compose(chainer: Chain) : Car {
return chainer.fold(Car(chainer)) { car, chainUnit ->
when(chainUnit) {
is CarEngine -> car.carEngine = chainUnit
is CarVisuals -> car.carVisuals = chainUnit
is CarSpecs -> car.carSpecs = chainUnit
is CarOwner -> car.carOwner = chainUnit
}
car
}
}
}
Our compose() function is the only way we can construct the car hence, safe to say that the modules would be present when instantiating a new Car. Yes, we can pass an empty Chain, which can be handled with a simple conditional check.
The result of the chained operations is the accumulator after folding through all the chain units. The accumulator is the final composed object (The car).
Furthermore, our Vehicle interface function implementations can now be completed since we have the necessary modules in our car class.
override fun isSafeForEnvironment(): Boolean {
return if(carEngine != null) {
!carEngine!!.causesPollution()
} else {
// default, handle as needed
false
}
}
In the above snippet, we use the car engine and call the causesPollution() function from CarEngine module to determine if the car is safe for our environment.
Extending and using the Chain
To use the chain, we’ll use Kotlin’s extending functions and leverage the then() function to chain our function calls.
fun Chain.carEngineType(engineType: EngineType) : Chain = this.then(
CarEngine(engineType)
)
fun Chain.carOwnerDetails(name: String, licenceNo: String, address: String) : Chain = this.then(
CarOwner(name,licenceNo,address)
)
fun Chain.carSpecs(wheelCount: Int, seats: Int, manufacturer: String) : Chain = this.then(
CarSpecs(wheelCount,seats,manufacturer)
)
fun Chain.carVisuals(color: String) : Chain = this.then(
CarVisuals(color)
)
Each extension function calls the linking then function from the Chain to build LinkedChain objects composing our final Car.
These extensions are for Car object you can build extensions for multiple objects as well.
The result
// Composing our car
val car = Car.compose(Chain.carEngineType(EngineType.ELECTRIC)
.carOwnerDetails(name = "Android Dev Community",
licenceNo = "API31",
address = "California"
).carSpecs(
wheelCount = 4,
seats = 2,
manufacturer = "Tesla"
).carVisuals(
color = "Green"
)
)
// Use the car
car.isSafeForEnvironment()
Our pattern is complete! We’ve built a function-chaining compositional pattern that can be used project-wide.
Summary
- Reusable chaining logic
- Modular composition of objects, easy coupling, and decoupling
- Extensible
- Compose() companion function can be customized
Here is a Github Gist for your reference, or a Kotlin Playground link to run the above code.
If you liked this post, and the chaining pattern, do share it and comment any relevant feedback down below. : )