1. Destructuring Pair and Triple
If you’re just starting to program, reading via first/second feels fine: “well, there are fields — so I just take them.” But pretty quickly the code turns into a small quest: where first is x, where first is min, and where first is “an error message”? Destructuring solves this in a simple way: you immediately give the components meaningful names, and after that the code reads like plain text.
The second reason is local readability. When you “unroll” a Pair into x and y, you no longer need to keep in your head what first meant. The variables become self-documenting. And this is one of those rare cases where the compiler doesn’t just complain — it also helps you write clearer code.
Pair: val (a, b) = pair
The most common option is to unpack a Pair into two variables. The syntax looks like we’re “assigning two variables at once,” although technically it’s still one expression on the right and one declaration on the left.
Let’s start with the simplest possible example:
fun main() {
val point = 10 to 20
val (x, y) = point
println("x=$x y=$y") // x=10 y=20
}
Notice two things. First, on the left we declared two variables x and y. Second, the meaning of first/second is now “baked into” the names: x is the first, y is the second, and the reader of the code is not required to remember that there’s a Pair sitting somewhere.
Destructuring vs first/second: when each is more convenient
Sometimes the logic is this: if you use both parts, then destructuring usually wins. If you need only one part, sometimes it’s easier to take pair.first or pair.second. To avoid arguing, you can think of it like this: destructuring is good when you will use the components separately many times later, and first/second are good when you take one component once and apply it right away.
Let’s compare with an example:
fun main() {
val range = 1 to 100
// Option A: without destructuring
println("from=" + range.first) // from=1
println("to=" + range.second) // to=100
// Option B: with destructuring
val (from, to) = range
println("from=$from to=$to") // from=1 to=100
}
Both options are correct. But in the second case, the “agreement” about the meaning of the components is moved into the names from/to, and this usually reduces the chance of mixing up the order.
Can you specify types in destructuring?
Yes, you can. Usually Kotlin infers the types, but sometimes it’s useful to make the type explicit so you yourself understand what’s going on (or so the compiler can point out an error earlier).
fun main() {
val data: Pair<Int, String> = 7 to "days"
val (count: Int, label: String) = data
println("$count $label") // 7 days
}
In practice, most of the time val (count, label) = data is enough, but the ability to write the type explicitly is a good “plan B” if you’re unsure.
Triple: val (a, b, c) = triple
Triple is the same thing, just with three components. And destructuring is especially nice here because first/second/third are even harder to read than first/second: the brain starts to treat them as “first/second/third something” rather than specific values.
An example with an RGB color:
fun main() {
val rgb = Triple(255, 128, 0)
val (r, g, b) = rgb
println("r=$r g=$g b=$b") // r=255 g=128 b=0
}
Here destructuring isn’t just “shorter,” it does the main thing: we removed the words first/second/third and replaced them with domain terms (r/g/b). That’s exactly why people like destructuring in real-world code.
2. Skipping components with _
Now an important trick: sometimes you need a Pair or Triple as a container to “carry everything at once,” but at a specific usage site you need only part of it. Creating a variable just for it to sit there as dead weight is undesirable — and the compiler doesn’t like it either: it will start warning you that the variable is not used.
For this, Kotlin has _ in destructuring: it means “I don’t need this component.” It’s not a variable; it is specifically a placeholder.
Example:
fun main() {
val result = "OK" to 200
val (status, _) = result
println(status) // OK
}
We skipped the second component (200). And it reads as an intentional decision: “I know there’s a code there, but I don’t need it right now.”
_ cannot be used as a variable
A very common beginner mistake: trying to reference _ later as if it were a variable name. This won’t work.
fun main() {
val p = 3 to 4
val (_, y) = p
println(y) // 4
println(_) // not allowed: "_" is not a variable
}
This strictness is only a plus: it protects you from accidentally using something you explicitly declared as “unimportant.”
_ is also useful as a hint to the reader
Sometimes beginners think: “what’s wrong with val (a, b) if b isn’t needed?” There’s nothing wrong with it; the program will work, but the reader of the code (including you a week later) won’t understand: is the component actually important but just not used yet, or is it unnecessary altogether? _ solves this honestly: “no, it’s not needed.”
4. How destructuring works: componentN()
Let’s figure out how all of this works — we’re programmers, aren’t we? Destructuring looks like special language syntax, but in reality the compiler just translates it into calls to special functions componentN().
The rule is simple: if an object can answer component1(), component2() (and so on), then it can be destructured. The Kotlin specification explicitly shows that destructuring relies on these component functions and that the unpacking proceeds in component order.
Example: manually instead of destructuring
What the compiler does automatically, we can write ourselves:
fun main() {
val p = 3 to 4
val a = p.component1()
val b = p.component2()
println(a + b) // 7
}
This is not “better” than val (a, b) = p, but it’s useful to see the mechanics. Destructuring is a convenient wrapper around component calls.
A mini-diagram of what happens
If we simplify, destructuring:
val (a, b) = pair
is roughly equivalent to this idea:
tmp = pair
a = tmp.component1()
b = tmp.component2()
And an important detail: a temporary variable tmp is created so that the expression on the right is evaluated only once. This is especially noticeable with nested destructuring (when “inside” there is also a Pair), and the specification illustrates how the compiler expands it into a sequence of componentN() calls with temporary variables.
We won’t dive into complex nested variants right now (so as not to turn the lecture into a matryoshka circus), but the principle is useful to understand: destructuring is syntactic sugar, not a separate memory model.
Limitation: destructuring is a declaration, not an assignment
In Kotlin, destructuring in this form works specifically as a declaration of new variables:
val (x, y) = point
But not as “overwrite existing x and y in one line.” That is, you can’t do “destructuring assignment” like in Python — here you’ll have to assign separately or redeclare variables in a new scope (for example, inside a block { ... }).
5. Practice: “ContactCard” using Pair
Let’s carefully integrate destructuring into our study project “write small functions and print the result.” Imagine we’re making a simple console stub for a “contact card”: name and phone number. For now, without complex architecture and without commands — just demonstrating the idea.
Create a Pair as a “contact” and unpack it nicely:
fun main() {
val contact = "Ada Lovelace" to "+44 123 456"
val (name, phone) = contact
println("Name: $name") // Name: Ada Lovelace
println("Phone: $phone") // Phone: +44 123 456
}
Now let’s make a version where the phone number isn’t needed (for example, we output only the name for a list):
fun main() {
val contact = "Ada Lovelace" to "+44 123 456"
val (name, _) = contact
println("Name: $name") // Name: Ada Lovelace
}
Notice how comfortable this is to read: you explicitly show the intent — take only the name.
6. Typical mistakes when destructuring Pair/Triple
Error #1: trying to destructure a Pair into three variables (or a Triple into two).
This is one of the most common “shape errors”: three names on the left, an object with two components on the right. The compiler won’t guess your thoughts and will honestly say that component3() is missing or, conversely, that you’re requesting too few components. Keep a simple mapping in mind: Pair is exactly two components, Triple is exactly three.
Error #2: using meaningless names a, b, c where the meaning matters.
Destructuring by itself does not automatically make code clear: it only gives you a chance to name things well. If you write val (a, b) = range, the reader will still be guessing what a and b are. If these are the bounds of a range — use from/to; if these are coordinates — x/y; if these are a status and a code — status/code.
Error #3: destructuring just for the sake of destructuring when you need one component.
Sometimes people write val (x, _) = pair even when it’s simpler and clearer to take pair.first. Formally it’s fine, but stylistically the option that “moves less code” usually wins. If you need one component once — take first/second/third. If the components will be used actively afterwards — destructure.
Error #4: forgetting that component order is a contract.
Destructuring unpacks strictly by order: the first goes into the first variable, the second into the second, and so on. It doesn’t “understand” what you meant. So if in one place you decided that minMax is (min, max), stick to that everywhere. Otherwise you’ll get a bug that looks like “everything works, but for some reason negative numbers sometimes become positive.”
Error #5: treating _ as a normal variable.
_ in destructuring means “discard the component,” not “name the variable underscore.” You can’t print it with println(_), pass it along, store it, and so on. If you suddenly realize you do need the component after all — just give it a proper name.
Error #6: not understanding the connection to componentN() and being scared by compiler messages.
Sometimes the compiler complains about a missing component2() or a type mismatch — and it can sound like an incantation from dark magic. But behind it is simple mechanics: destructuring = calling componentN() in order. If you remember this connection, compiler messages become translatable: “can’t extract the second component” or “the second component has the wrong type.”
GO TO FULL VERSION