Should kotlin support nested deconstruction?

In certain situations I end up wanting nested deconstruction like below (simplified example):

val ((x, y), z) = (1 to 2) to 3

This doesn’t work for me and I have to rewrite the code as such:

val (tmp, z) = (1 to 2) to 3
val (x, y) = tmp

Especially for lambdas nested deconstructing would improve the code:

f { ((x, y) , z) -> x + y + z }

Over:

f { ((tmp , z) -> 
  val (x,y) = tmp
  x + y + z }
18 Likes

I think it is a bad idea.

  1. kotlin usese componentX methods for destructuring declaration. Doing something more complicated is imposible in current model. Chaging model could ruin a lot of other things in the language.
  2. I do not see any reason to do so. If you have a complex return type, then create a complex return type like
data class Tmp(val x: Double, val y: Double)
data class Result(val tmp: Tmp, val z: Double)

And now you can accept return value like this:

val res = Result(Tmp(x,y), z)

It is not much more complicated, but solves a lot of potential problems like type checks and IDE support.

By the way, your last example could be rewritten as

f { (tmp, z) -> tmp.key + tmp.value + z }

or

f { (tmp, z) -> tmp.x + tmp.y + z }

if you use object with appropriate properties for tmp.

1 Like

Why is it impossible? It doesn’t break the grammar, just change

variableDeclarationEntry
  : SimpleName (":" type)?
  ;

to

variableDeclarationEntry
  : SimpleName (":" type)?
  : multipleVariableDeclarations
  ;

Seems like any argument against nested deconstruction would also apply to first-level deconstruction. Kotlin might only have allowed:

    f { result -> result.tmp.x + result.tmp.y + r.z }

As it is, deconstruction destructuring seems half-done.

4 Likes

I did not said that it is impossible in principle, I said it is a bad idea.
I am not talking about grammar, but about backing entity. Say, you want to introduce a new class that could be used in destructuring declaration. For now you just have to implement componentN methods. Now how can one implement proposed construct? Probably it is possible to find componentN methods in resulting objects. But what if resulting object is generic? It requires a whole layer of compile-time type analysis.

It is of course my own opinion, but I think that one should not turn kotlin into scala by adding tons of language features. If one really need nested destructuring declarations, he can write a compiler plugin for that. In my experience, requirement for such structures either mean that you need to used dynamic language (like groovy), or it is just a bad design.

1 Like

Probably it is possible to find componentN methods in resulting objects. But what if resulting object is generic?

Then perhaps it can’t be done for generics. Kotlin can’t fix type erasure.

If one really need nested destructuring declarations, he can write a compiler plugin for that.

It’s not a need, its an opportunity for better clarity of expression:

data class Person(val id: Int, val name: String)
data class Employee(val person: Person, val store: String)

val f1: (Employee) -> String = { (person, store) -> "${person.name}, $store" }
val f2: (Employee) -> String = { ((_, name), store) -> "$name, $store" }

f2 is cleaner because it removes irrelevant symbols from the namespace.

I think that one should not turn kotlin into scala by adding tons of language features

Then let’s push to remove destructuring.

7 Likes

Declaration destructuring is a compile-time feature and type erasure appears only in runtime, so it is probably could be done, but that does not mean it should be done.

1 Like

Cool. Thanks.

So what is your argument that Kotlin should destructure Employee but not Person in the example above?

1 Like

Because it is needless complication of the compiler? Could you show a use case where this type of destructuring is clearly needed? In the example you showed above, the f1 is in my opinion better than f2. It has better readability and allows for explicit type definition (and therefore check).

If you will say, that it is just more beautiful, then I will say that beauty itself is subjective and if you just try to invent beautiful constructs, sooner or later someone will propose to designate code blocks by spaces.

Here’s simple example:

  for ((id, point) in points) {
    val (x, y) = point
  }

It would be better to write

  for ((id, (x, y)) in points) {
  }

I don’t really see any problems with this suggestion. Not the most important feature, of course, but it fits nicely with language.

23 Likes

Because it is needless complication of the compiler

If you want to reduce compiler work, you should write .class files by hand.

In the example you showed above, the f1 is in my opinion better than f2.

The person object is not relevant to the function’s behavior so there is no need for it to be passed in.

and allows for explicit type definition (and therefore check).

The outermost type must already be supplied in Kotlin. But if you wanted to supply additional types, you could supply them for the compiler to double-check, similar to what it already does for 1st level destructuring: ((id: Int, name: String): Person, store: String): Employee.

If you will say, that it is just more beautiful

I did not invoke aesthetics. My reason for preferring f2 is practical: fewer irrelevant symbols means better readability.

4 Likes

Sometimes it’s not as easy or not desirable to create new types.

For exampe I use a home-grown Result type that is essentially a more advanced Maybe type (haskell) in that it supports error messages in addition to a value or nothing.

My code looks like this:

protocol and serverName and serverPort then { p ->
    doSomething (p.first.first, p.first.second, p.second) 
  }

This combines the results and iff the aggregated result is good it will invoke the lambda.

In these cases I don’t find it compelling to create types to hold the nested tuple data. In addition p.first.first isn’t readable.

With nested deconstructing I could write:

protocol and serverName and serverPort then { ((protocol, serverName), serverPort) -> 
    doSomething (protocol, serverName, serverPort) 
  }

Which I would prefer.

Nested deconstruction has been part of ML-like languages for a long time so there is prior art. As a developer with some ML experience I find frustrating that nested deconstruction is not supported in kotlin.

I haven’t considered implementation detail as I think the first question that should answered is if nested deconstruction is something that should be supported.

4 Likes

Looks like you’re trying to bring back tuples, which Kotlin intentionally jettisoned. I think a strong case could be made that destructuring is a better fit for tuple-like data, and so maybe both features really should have been removed together.

The code doesn’t intend to create n-tuples instead and creates nested pairs (2-tuple). As we conveniently can create nested pairs I would like a convenient way to deconstruct them.

A “trick” to avoid nested tuples is to implement the Applicative functional pattern but that relies on partial functions which AFAIK kotlin doesn’t support.

What I can do in kotlin to fix the code above is creating a bunch of overloads for combinations of 1,2,3,4,5 … N results. (Like in C++ before variadic templates but unlike C++ we don’t have macros that simplify creating the overloads).

I agree that deconstruction as is today in kotlin only really makes good sense when dealing with tuples. If one supports nested tuples (like pairs) it make sense to me to support nested deconstruction of those.

With that said in ML like languages nested deconstruction is also used in pattern matching for union types and tuples and is very useful there.

3 Likes

I’ve found a few cases where I need to work with triples from lists which I can select using the zip method and this would suddenly be very useful.

Now,

val x = listOf(1,2,3)
val y = listOf(4,5,6)
val z = listOf(7,8,9)

x.zip(y).zip(z).forEach {/* Function using it.first.first, it.first.second, and it.second */}

or

x.zip(y).zip(z).forEach {(a,b) -> /* Function using a.first, a.second, and b */}

But with nested destructuring, I could do

x.zip(y).zip(z).forEach {((a,b),c) -> /* Function using a, b, and c *}

Both the first and second approaches feel ugly, but the last feels much cleaner.

12 Likes

Totally agree with TS that this looks like a half-baked feature without nested destructuring which was an easy thing to do actually! All arguments against it in this thread are either non-sense or lead to the argument that destructuring is a useless feature which should be removed from the language altogether.

6 Likes

I agree as well. Nested Destructuring seems more logical than not having it. I do not quite understand why it is seen as a language feature that would make things more complicated. It seems like an unnatural “restriction” that nested destructuring is not possible.

I think the pattern is quite clean, clear and straightforward:

  • the parentheses are used to destructure into all its children
  • the children of destructuring parentheses can be parentheses as well

Or, in other words, I think the behaviour of this feature is kind of self-explanatory.


Another interesting use case for this is iterating over a map with indices:

Normally, you would iterate over a map like this:

for ((key, value) in map) {
    // ...
}

If you also want indices, you can use withIndex() (has to be used on the entryset):

for (obj in map.entries.withIndex() {
     // 'obj' is a IndexedValue<Set<Map.Entry<K, V>>>
    obj.index // this is the index
    obj.value // this is the entry
    obj.value.key // this is the key
    obj.value.value // this is the value
}

Of course, you can use destructuring here but only on the top level:

for ((index, entry) in map.entries.withIndex()) {
    // 'index' is now destructured, but key and value are not 
}

I think it would only be logical if the following could be done as well:

for ((index, (key, value)) in map.entries.withIndex()) {
    // ...
}
5 Likes

I’m not sure that adding this as a full-fledged language feature is a good idea. It’s confusing enough having to remember what each componentN() function is and prevent bugs due to mixups. There’s a KEEP for name-based destruction which I think would be much better than index-based destruction.

In any case, for those who need it, you can add extensions. For example, if I have a Pair<A, Pair<B, C>> this can be done:

typealias Pack = Pair<A, Pair<B, C>>

private operator fun Pack.component3(): B = second.first
private operator fun Pack.component4(): C = second.second

val useObj: (Pack) -> Unit = { (a: A, ignore: Pair<B, C>, b: B, c: C) -> Unit }

The drawback is the throwaway object that takes an index and has to be ignored using _.

If using this approach, you should scope the extensions appropriately (nest inside the using function, or private in the using file) and have a typealias - messing with nested generics is prone to bugs/typos.

2 Likes

I second and echo these precise sentiments: Not having nested destructuring is half-baked and an easy thing to do.

I especially agree that all arguments against it in this thread, especially the painful first few posts in the thread, are disingenuous in that they ignore the self-contradiction that lead to the argument that destructuring is a useless feature which should be removed from the language.

I desperately felt the need for nested destructuring after writing just a couple of small programs. As someone wise pointed out – it is NOT AESTHEICS – the problem is the pollution of the namespace by an unnecessary symbol to work around the lack of this simple feature.

Come on, other members of the Kotlin team – let’s be aspirational as that’s what has led Kotlin to be amazing so far. Let’s not slide back into the medieval way of thinking.

red pill:

fun main () {
    listOf(
        "london" to "england",
        "paris" to "france",
        "berlin" to "germany"
    ).withIndex().map { (index, UGLY_TEMP_POLLUTANT) ->
        val (city, country) = UGLY_TEMP_POLLUTANT
        println("$index: capital of $country is $city")
    }
}

versus blue pill:

fun main () {
    listOf(
        "london" to "england",
        "paris" to "france",
        "berlin" to "germany"
    ).withIndex().map { (index, (city, country)) ->
        println("$index: capital of $country is $city")
    }
}
9 Likes

This is going on my list of most painful conversations I have had the misfortune to read. It’s already sad that fully fleshed out object destructuring does not exist in kotlin yet; hell, even Java is on the verge of supporting it.

I know there are problems with the current implementation of destructuring, but that’s what KEEPs are for, to suggest improvements and further the language. and there has been a good amount of discussion about it. The kotlin team has tacitly agreed that pattern matching is something on the road map, and I hope they can expedite the process.

Denying the utility of this feature is just inexcusable at this point. Please stop gaslighting yourselves.

6 Likes