44

I am looking for a clean way to create destructurable objects in-line. kotlin.Pair and kotlin.Triple cover a lot of use cases, but sometimes there are more objects that are needed to be passed.

One sample use case is RX's zip function, where the results of several I/O calls need to be mapped into another object:

Single
    .zip(repositoryA.loadData(someId),
         repositoryB.loadData(someId),
         repositoryC.loadAll(),
         repositoryD.loadAll()),
         { objectA, objectB, objectsC, objectsD -> /*some Kotlin magic*/ }
    )
    .map { (objectA, objectB, objectsC, objectsD) -> /*do the mapping*/ }

I am trying to figure out what would go in the "some Kotlin magic" part. If there were only 3 repositories, it would be

Triple(objectA, objectB, objectsC)

Do I need to create a new data class for this, and for any n-tuple case, or is there another way?

kenny_k
  • 3,831
  • 5
  • 30
  • 41
  • Beyond Triple, you really should consider using a data class anyway. It's unlikely that the data you're sending is adequately modeled by a tuple at that point – aeskreis Mar 10 '22 at 20:35

4 Answers4

52

Basics

Let's see how destructuring works:

Kotlin defines a convention for this, i.e. componentX() operator functions are an example of the principle of conventions used in Kotlin in many places. These componentX() functions are used by the compiler for the initialization of variables in destructuring declarations.

For example in Pair<A,B> these functions look as follows:

operator fun component1(): A = first 

operator fun component2(): B = second

As you can see these are operators, specially handled functions. These componentX() functions can be provided by the developer and will automatically be produced by the compiler for data classes. Pair also is such a data class btw.

Answer

Thus, just go ahead and use data classes whenever you need more than a Triple.

For example, a class MultiComponent defined as this:

data class MultiComponent(val x: Int, val y: Int, val z: Int, val a: Int, val b: Int, val c: Int)

will be compiled to a class with functions component1(), component2(), ..., component6() and can be used in destructuring declarations:

val (q, w, e, r, t, z) = MultiComponent(1, 2, 3, 4, 5, 6)
Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
s1m0nw1
  • 76,759
  • 17
  • 167
  • 196
  • This is nice feature of kotlin. I like kotlin more day by day, because most python features are implemented mostly somehow same way. Not exactly same way, so somehow anyway. One pythonism is methods/functions that return many values This is very difficult to implement by java, but quite easy with kotlin by data class. data class MultiComponent(objectA:A, objectB:B, objectsC;c, objectsD:D) fun someFunction(...):Multocomponent – Reijo Korhonen May 01 '23 at 13:27
35

I found it easiest to just code-gen the n-tuples needed. Use-case was functional extension methods for things like memoization, etc.

data class NTuple2<T1, T2>(val t1: T1, val t2: T2)

data class NTuple3<T1, T2, T3>(val t1: T1, val t2: T2, val t3: T3)

data class NTuple4<T1, T2, T3, T4>(val t1: T1, val t2: T2, val t3: T3, val t4: T4)

data class NTuple5<T1, T2, T3, T4, T5>(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5)

data class NTuple6<T1, T2, T3, T4, T5, T6>(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6)

And then generate the necesssary construction-helpers:

infix fun <T1, T2> T1.then(t2: T2): NTuple2<T1, T2>
{
    return NTuple2(this, t2)
}

infix fun <T1, T2, T3> NTuple2<T1, T2>.then(t3: T3): NTuple3<T1, T2, T3>
{
    return NTuple3(this.t1, this.t2, t3)
}

infix fun <T1, T2, T3, T4> NTuple3<T1, T2, T3>.then(t4: T4): NTuple4<T1, T2, T3, T4>
{
    return NTuple4(this.t1, this.t2, this.t3, t4)
}

infix fun <T1, T2, T3, T4, T5> NTuple4<T1, T2, T3, T4>.then(t5: T5): NTuple5<T1, T2, T3, T4, T5>
{
    return NTuple5(this.t1, this.t2, this.t3, this.t4, t5)
}

infix fun <T1, T2, T3, T4, T5, T6> NTuple5<T1, T2, T3, T4, T5>.then(t6: T6): NTuple6<T1, T2, T3, T4, T5, T6>
{
    return NTuple6(this.t1, this.t2, this.t3, this.t4, this.t5, t6)
}

So I could then do:

val nTuple4 = 1 then 2 then "foo" then "bar"

Resulting in:

val nTuple4: NTuple4<Int, Int, String, String>
Dan Lugg
  • 20,192
  • 19
  • 110
  • 174
  • 3
    nice use of infix functions. though at that point could also just define an overloaded `tuple(..)` method. the repeated `then` makes it kinda long and verbose. – 0cd Feb 09 '21 at 01:33
  • Agreed, but the real value is when you already have a tuple; such as `val nTuple6 = nTuple4 then 7 then 8` — but indeed, a constructor-helper would be helpful too. – Dan Lugg Feb 14 '21 at 07:26
  • I think I misunderstood your comment @0cd — rereading I believe you were referring to overloading `rangeTo` to use `..` between tuple components? If so, absolutely! The only issue is existing overloads for `Int`, wherein `1 .. 2` would result in a range, rather than an `NTuple`. You’d have to do `NTuple(1, 2) .. 3`. Otherwise agreed, it’s much less verbose. – Dan Lugg Mar 11 '21 at 15:25
  • oh, i just meant to say one could create methods like tuple(T1 v1), tuple(T1 v1, T2 v2) and so on. I realized now I shouldn't have used the .. as that could lead to confusion. I like the "then" version that you have too :) .. was just sharing another option. – 0cd Mar 12 '21 at 19:29
17

In contrast to Scala, Kotlin does not have n-tuples for values higher than 3 defined. You've correctly identified Pair and Triple.

Kotlin favors using data classes for these use cases, according to this blog post. So yes, you'll have to define a data class in order to do what you want, there is no Quadruple. I'd personally argue that defining your own data class is more clear, and will end up being compiled and used the same way a hypothetical Quadruple would anyway, under the covers.

As for destructuring data classes, Kotlin supports that as well:

data class Thingy(val a: String, val b: String, val c: String, val d: String)
val t = Thingy("A", "B", "C", "D")
val (aa, bb, cc, dd) = t
Todd
  • 30,472
  • 11
  • 81
  • 89
17

You can add these kotlin classes to your project, they will work exactly like Pair or Triple but you will be able to pass more objects.

Quadruple (lets you pass 4 objects)

import java.io.Serializable

/**
 * Created by nalcalag on 09/02/2019.
 * 
 * Represents a quartet of values
 *
 * There is no meaning attached to values in this class, it can be used for any purpose.
 * Quadruple exhibits value semantics
 *
 * @param A type of the first value.
 * @param B type of the second value.
 * @param C type of the third value.
 * @param D type of the fourth value.
 * @property first First value.
 * @property second Second value.
 * @property third Third value.
 * @property fourth Fourth value.
 */
data class Quadruple<out A, out B, out C, out D>(
        val first: A,
        val second: B,
        val third: C,
        val fourth: D
) : Serializable {

    /**
     * Returns string representation of the [Quadruple] including its [first], [second], [third] and [fourth] values.
     */
    override fun toString(): String = "($first, $second, $third, $fourth)"
}

/**
 * Converts this quadruple into a list.
 */
fun <T> Quadruple<T, T, T, T>.toList(): List<T> = listOf(first, second, third, fourth)

Quintuple (lets you pass 5 objects)

import java.io.Serializable

/**
 * Created by nalcalag on 09/02/2019.
 * 
 * Represents a quintet of values
 *
 * There is no meaning attached to values in this class, it can be used for any purpose.
 * Quintuple exhibits value semantics
 *
 * @param A type of the first value.
 * @param B type of the second value.
 * @param C type of the third value.
 * @param D type of the fourth value.
 * @param E type of the fifth value.
 * @property first First value.
 * @property second Second value.
 * @property third Third value.
 * @property fourth Fourth value.
 * @property fifth Fifth value.
 */
data class Quintuple<out A, out B, out C, out D, out E>(
        val first: A,
        val second: B,
        val third: C,
        val fourth: D,
        val fifth: E
) : Serializable {

    /**
     * Returns string representation of the [Quintuple] including its [first], [second], [third], [fourth] and [fifth] values.
     */
    override fun toString(): String = "($first, $second, $third, $fourth, $fifth)"
}

/**
 * Converts this quintuple into a list.
 */
fun <T> Quintuple<T, T, T, T, T>.toList(): List<T> = listOf(first, second, third, fourth, fifth)
Noelia
  • 3,698
  • 4
  • 17
  • 22