1. Overview

When working with Kotlin, we may sometimes want to group three elements seamlessly.

In this tutorial, let’s learn about Kotlin’s Triple type and discover the elegance triples bring to our code.

2. The Triple Class

The Pair class can be an ideal choice when we need to group two elements. Triple is quite similar to Pair, except that it holds one additional element, as its name implies.

Let’s first take a look at how Triple is defined:

public data class Triple<out A, out B, out C>(
  public val first: A,
  public val second: B,
  public val third: C
) : Serializable

As the code above shows, Triple is a data class. That is to say, Triple is final and cannot have a subclass. Further, Triple is immutable since all three properties are defined as val.

We can put three values from different types in a Triple, for example, Triple(42, “Kotlin”, Long.MAX_VALUE). Moreover, Triple supports destructuring declarations:

val (a, b, c) = Triple(42, "Kotlin", Long.MAX_VALUE)
assertTrue { a is Int }
assertEquals(42, a)

assertTrue { b is String }
assertEquals("Kotlin", b)

assertTrue { c is Long }
assertEquals(Long.MAX_VALUE, c)

Next, let’s see which standard functions the Triple class provides.

3. The toString(), toList(), and Custom Extension Functions

We’ve noted that the Triple class is a data class. Therefore, all standard functions Kotlin provides for data classes are available for the Triple class, for example, the copy() function.

It’s worth noting that Triple overrides the data class’s default toString() function:

public override fun toString(): String = "($first, $second, $third)"

As we see, the toString() function returns the string representations of the three elements enclosed within parentheses, making it easier to identify the object as a Triple:

val t = Triple("A", "B", "C")
assertEquals("(A, B, C)", t.toString())

We can use Kotlin’s string templates instead of calling the toString() function explicitly to enhance the readability:

assertEquals("(A, B, C)", "$t")

Additionally, Triple has a builtin extension function toList():

public fun <T> Triple<T, T, T>.toList(): List<T> = listOf(first, second, third)

As its name implies, toList() converts the three elements in the Triple object to a list:

val t = Triple("Java", "Kotlin", "Python")
assertEquals(listOf("Java", "Kotlin", "Python"), t.toList())

Notably, the toList() function is only available for triples whose elements are of the same type.

In practical scenarios, we can craft custom extension functions to tailor the Triple class to suit our specific requirements better.

For instance, we can create the reverse() extension to reverse elements in a triple:

fun <T> Triple<T, T, T>.reverse() = Triple(third, second, first)

// usage:
val t = Triple("x", "y", "z")
assertEquals(Triple("z", "y", "x"), t.reverse())

4. An Example of Triple Usage

Finally, let’s see a Triple usage example to understand how we can use this data type.

Let’s say we have the Player class:

data class Player(
  val name: String,
  var score: Int
)

We also have a few Player objects:

val kent = Player("Kent", 42)
val eric = Player("Eric", 42)
val tom = Player("Tom", 20)
val john = Player("John", 32)

Now, we want to have a simple “match” construct. A match has three elements: two players and a time to begin. Of course, we can model it as a class. However, if we want to keep it lightweight, Triple is a good option too:

val matchesTomorrow = listOf(
  Triple(kent, tom, "9:00"),
  Triple(eric, john, "10:00"),
  Triple(kent, eric, "17:00"),
  Triple(tom, john, "18:00"),
)

For example, we can find which players are involved in the match at 10:00:

val matchAt10 = matchesTomorrow.first { it.third == "10:00" }
assertEquals(eric, matchAt10.first)
assertEquals(john, matchAt10.second)

If we like, we can create a type alias:

typealias Match = Triple<Player, Player, String>

In this way, we can use the type alias Match for Triple<Player, Player, String> to make our code easier to read:

//using type alias
val matchesTomorrow2 = listOf(
  Match(kent, tom, "9:00"),
  Match(eric, john, "10:00"),
  Match(kent, eric, "17:00"),
  Match(tom, john, "18:00"),
)
                                                              
val matchAt17 = matchesTomorrow2.first { it.third == "17:00" }
assertEquals(kent, matchAt17.first)
assertEquals(eric, matchAt17.second)

Furthermore, we can create extensions to our Match alias, which actually extend the Triple class to support more functionalities. Let’s create the firstWin() and secondWin() extensions to update players’ score accordingly:

fun Match.firstWin() = apply {
    first.score++
    second.score--
}
                               
fun Match.secondWin() = apply {
    first.score--
    second.score++
}

Then, we can call the extension functions conveniently:

matchAt17.secondWin()
                                        
assertEquals(41, matchAt17.first.score)
assertEquals(43, matchAt17.second.score)

5. Conclusion

In this article, we’ve explored Kotlin’s Triple class, delving into its functionalities and understanding when and how to leverage it. Additionally, we’ve seen the practical application of Triple through a usage example.

As always, the complete source code for the examples is available over on GitHub.