So I am in the process of making myself a library for code i frequently use, and some of the methods would accept pairs, the problem is, not all project I use the library in has Kotlin, some are pure Java, so I would have a Pair class for pure Java projects aswell, but now Im thinking: wouldn’t be nice to have an interface that Tuples implement so we can make it compatible with non-Kotlin pairs? A method would accept the Pair interface, and anything can extend it.
Or maybe just make Tuples open so we can build on top of it?
Why not make data classes, or soon make value/record classes?
Kotlin used to have tuples back in a beta version many years ago but they were removed. Tuples in a typed language a different thing than tuples in a dynamic language.
In Kotlin, you’re probably best off naming the kind of structured data instead of passing pairs around between functions. This helps with clarity by being explicit and helps to distinguish between kinds of pairs that aren’t really the same in meaning. For example, you should make a Point class to hold lat/lon data instead of reusing Pair.
Is there some specific api you want to share between all of your two value container types that would live in the interface? With data classes you’ll get the destructuring. And you probably shouldn’t be reusing names like “first” and “second” for the values across different kinds of two value container types.
So youre suggesting to not use the Pair and Triple Kotlin comes with at all?
I think that’s an excellent advice.
Almost, yes.
There are plenty of great usages for pairs in Kotlin. For example, in functional operations.
However, if you ever find yourself creating an API that isn’t meant to pass around specific kinds of data, you should probably not use Pair.
This reminds me of another classic anti-pattern I used to see in a code base. The authors had structured, typed, data but instead of creating a small class, they threw it all in a Map with string keys. This is similar because in doing this, we take away the distinction that enables the compiler to distinguish the structured data from kind another structured data.
Another way of putting it, imagine I have a function registerUser
that returns a string username and a string password. Here’s the signature using a Pair<String, String>
fun registerUser(): Pair<String, String>
Let’s also define another function nextAddress()
that returns a string zip code and a string street name as a Pair<String, String>:
fun nextAddress(): Pair<String, String>
What we’ve done by using Pair is thrown away the best feature of Kotlin and any types language, we’ve thrown out most of the power of the type system by Stringly Typed (but with Pair instead of String. Same principle though) these two completely different kinds of data, user credential and address.
A better way would be to define UserCredential
and Address
classes. Which would make the functions
fun registerUser(): UserCredential
fun nextAddress(): Address
Notice that the code is far more self-documenting now that we’ve named these two distinct kinds of data with a nice human-readable label other than “pair”.
There’s one final step we might want to take to push our usage of the type system even further. Consider our user credential class of
data class UserCredential(val username: String, val password: String)
You might notice that the string of a username and a string of a password are similar in that both of their content, or the data format, is made up of a string (char sequence). While this is true, the backing data is stored in the same underlying format, a username is NOT the same kind of thing as a password. They aren’t interchangeable and may need to be treated differently.
The type system can help us here. We’ll make a password class like this:
value class Password(val content: String)
This wrapping class enables the compiler to enforce the distinction between a password and a string.
In some cases, this will be worthwhile doing for values like passwords. In other cases, it’s not worth the distinction. For our example, let’s imagine we don’t use the zip code and we decide it’s not worthwhile making a zip code class–for another project that deals more with zipcodes we’d probably invest the 30 seconds of time to create a zipcode class.
I’d always recommend at least defining at least the first layer of kinds of data (i.e. Address
level) and then deciding to make wrapper classes at the lowest levels based on how widespread that kind of data is used.
For a real-life example, I once made a Latitude
and Longitute
class to help avoid those two very different things being interchanged and allow me to add validation and rules to them individually.
EDIT:
One last example of using the type system in a powerful way: GitHub - kunalsheth/units-of-measure: Type-safe dimensional analysis and unit conversion in Kotlin.
After seeing how enforceablely correct code can be using a type system and libraries like this one, it makes me sad whenever I write code where all of my math is working with non-interchangeable kinds of Doubles and Ints