This article was first published in December 2022 and was updated in December 2024.
Kotlin’s sugar coating is great, but too much sugar can seriously hurt your health. In this blog post, we’ll discuss just that: why pairs and triples in Kotlin are an anti-pattern, and why you shouldn’t use them.
Introduction to pairs and triples
In Kotlin, pairs and triples are handy tools for bundling together two or three values, respectively. These data structures are helpful when you need to return multiple values from a function or combine several values without the overhead of creating a formal data class. For instance, if you want to return both a user’s name and age from a function, a pair can do the job neatly. Similarly, a triple can handle three values, like a user’s first name, last name, and age.
Pairs and triples are part of the Kotlin standard library, making them readily available and widely used in Kotlin programming. They offer a quick way to group multiple values, but as we’ll see, this convenience comes with its own set of drawbacks.
Using pairs and triples
Let’s say you have a form to collect the first and last name of a user, and you need to pass them to another screen. A convenient way would be to create a Pair
:
val user : Pair<String, String> = Pair("John", "Doe")
What if you want to collect and pass the user’s age as well? You could use a Triple
instead of a Pair
:
val user : Triple<String, String, Int> = Triple("John", "Doe", 20)
Simple, right? This is where the convenience ends, for several reasons. First, if you’re passing this to another screen, it’s easy to get confused about what these properties mean. Take a look at the following example:
if(user.third > 21) { setEligible(user.first, user.second) }
If you were to see this chunk of code without previous knowledge of how the Triple
was created, you could easily be confused with its meaning. What is user.third
and why should it be over 21? What are user.first
and user.second
, and why are they parameters for the setEligible()
function? Pairs and triples are easy to write, but they’re often confusing to read and later interpret. This is always a red flag that something should be changed in the code.
This is also where your expandability possibilities end — there’s no Quadruple
or anything of that sort. If you want to add another property, you’d have to create your own data class. Or if you’re too lazy, you could go with a more radical solution, such as Triple<Pair, Int, String>
(where the pair contains the first and last name), which would make things even more confusing:
if(user.second > 21) { setEligible(user.first.first, user.first.second) }
So if pairs and triples are problematic, why do we use them?
Pairs and triples under the hood
We create pairs and triples because we want to avoid creating our own data classes. But let’s dig into the Tuples.kt
class, which provides the implementation of Pair
and Triple
, to see what they really are:
public data class Pair<out A, out B>( public val first: A, public val second: B ) (...) public data class Triple<out A, out B, out C>( public val first: A, public val second: B, public val third: C )
We avoid writing data classes and instead write… data classes?
Limitations of pairs and triples
While pairs and triples can provide a quick way to group two or three values together, they have notable limitations, outlined below.
-
Limited capacity
Pairs can only hold two values, and triples can hold up to three. If you need to handle more than three values, there’s no built-in support for quadruples or larger groups, which means you’ll need to design your own data structures or resort to workarounds.
-
Lack of named components
Elements in pairs and triples are accessed through generic property names like first
, second
, and third
. This can lead to code that is less intuitive and harder to understand. For instance, user.third
gives no immediate indication of what third
represents without additional context.
-
Not suitable for complex data
Pairs and triples are simple containers without any built-in support for adding methods or additional functionality. This limits their flexibility, making them less ideal for more complex or extensible data structures.
Overall, while pairs and triples are useful for quick, ad hoc grouping of values, their lack of scalability, readability, and extensibility can lead to maintenance challenges in larger codebases.
Data classes as a superior alternative
To address the limitations of pairs and triples, Kotlin offers data classes, a robust and flexible way to define simple or complex data structures. Some of the features of data classes are outlined below.
-
Named components
Data classes allow you to name properties, making your code more readable. For example:
data class Person(val firstName: String, val lastName: String, val age: Int)
Accessing properties with meaningful names is much clearer:
val user = Person("John", "Doe", 20) if (user.age > 21) { setEligible(user.firstName, user.lastName) }
-
Extensibility
Data classes enable you to add methods and additional functionality, making them suitable for representing complex data structures and business logic.
-
Maintainability
With their clear structure and named properties, data classes improve code maintainability, reducing ambiguity and potential errors.
While creating data classes may require a bit more upfront effort, the benefits in terms of readability, scalability, and maintainability make them a superior choice over pairs and triples for most use cases.
Conclusion
Pair
and Triple
are just data classes that are (arguably) more convenient than making our own data class, but there’s not much sense in using them. Although there’s nothing wrong with how Pair
and Triple
work, they violate the O in SOLID, in that they’re not open for extension. Additionally, they confuse code reviewers, and they could possibly even confuse future-you. Yes, making a completely new data class does take more time, but it contributes to improved code readability.
FAQ
Here are a few frequently asked questions about pairs and triples.