Position based declaration destructuring

Hi guys,

Kotlin is designed to be a very safe language, but for a some reason it includes very error prone feature: position based declaration destructuring.

Other languages (like ES6/TypeScript) use property name based destructuring and are safe for properties reordering, but Kotlin is not. (please correct me if I’m wrong)

Here is an article that describes the problem: Kotlin destructuring considered harmful | by David Stocking | Medium

What was the reason to select position based destructuring over name based one?
Is there any chance to get rid of it in one of the future releases?

7 Likes

Is positional based method invocation harmful?

Why

val address = Address(street, city, state)

is safe, while

val (street, city, state) = address

is harmful?

However a syntax enhancements looks interesting, associative destructuring can easily interoperate with plain Java bean.

2 Likes

Yes, as far as I understand if you change Address class to

val address = Address(street1, street2, city, state="")

The second line will still compile and have wrong data in ‘city’ and ‘state’ fields:

val (street, city, state) = address


About the question why constructor is not harmful (IMO), but destructuring is:

Most people come to Kotlin from Java. In Java if you have data object and want to add a new field a position of the new field does not matter. Only functional interface matters (fields order in constructor). In Kotlin field position is very important and this can be a surprise: adding, removing or a simple change of fields order may be missed by compiler and lead to illegal behavior in runtime.

Moreover, a lot of those who used to properties destructuring learned it from Javascript flavors (IMO). And in Javascript destructuring works by field names, not by order.

Name-based destructuring sounds like a good idea, I agree. How would it work if you wanted to destructure two instances of the same class in the same scope, or otherwise do destructuring which would create name clashes? Do other languages like JavaScript (or LISP/Scheme) have some way to add a prefix to the names, or something?

There is an easy and elegant way to rename variables during destructuring: 10. Destructuring

Below are syntax examples inspired by ES6 or even SQL

  • ES6 style: val (s: street, c: city, state) = address
  • SQL style: val (street as s, city as c, state) = address
  • One more SQL style: val (street s, city c, state) = address

The important part is that syntax with rename does not conflict with existing position based one.

2 Likes

I think both lines are equally unsafe, both the positional method / constructor call and the position based destructuring.
However for constructor / method call we have an alternative using named arguments, whereas we don’t have the alternative for destructuring assignment.

I know the feature from Python, where it’s based on Tuples and thus inherently position-based (and inherently unsafe).

It was very useful in Python but I find myself hardly using it in Kotlin, perhaps due to the fact that it doesn’t work with Tuples like Python. Thus you anyway need a (data) class and then you might just as well work with that class directly.

And I find the ‘with(…) {…}’ construct much more useful, since it doesn’t require extra variable assignments and is by definition name-based.

4 Likes

We have no plans to remove position-based destructuring from the language.

5 Likes

You can write your own name-based destructuring if you are worried about errors, but I can imagine it is easy to forget to write it as this:

val (street, city, state) = address.run { arrayOf(street, city, state) }

With this you can destructure anything, not just types that have componentN() functions. And you can use expressions to transform the values.

2 Likes

The question about removal was rather rhetorical and emotional. Backward incompatible changes remove trust into language (IMO).

The useful outcome from this topic could be a discussion of name based destructuring support in the language.
Could we use the same syntax as in ES6 to support this feature: curly brackets instead of parens:

val {street, city, state} = address

Is there any chance to support the feature this way in the language?

4 Likes

In order to be included into the language somebody has (first) to demonstrate some use-cases where the proposed feature is useful and adds something new. So far, it does not seem that anyone has demonstrated how the proposed syntax is better than with(address) { ... } or address.apply { ... }, both of which you can do now, and where inside of ... you can simply refer to street, city, and state by their name (as opposed to position) without having to violate DRY principle (no need to repeat the names)?

2 Likes

I think that there are potentially two use-cases:

  1. Mapping from a var to a val, so that compiler will not give errors about values that might have changes, breaking smart casting etc

  2. If you need values from multiple structures at the same time, and the names are shadowed. You can nest the with() {…} blocks but if names are shadowed, how would you refer to a name from the outer block? (Might be possible but I’m not sure how to do it without referring to the full variable it’s part of - defeating the use of with() {}

I’ll try to come up with some concrete code examples.

NB: I think the idea of name-based destructuring is potentionally very powerful and useful, and could be added without removing current position-based destructuring.

1 Like

I have nothing against position-based destructuring, I’m just disappointed that Kotlin doesn’t give a compiler error after a new property is added. A destructuring statement should ensure that all items are accounted for in order to compile.

2 Likes

I understand its convention, but nested apply means that if I want something logical like (if we use React as an example):
using kotlin:

props.apply {
      // need to use first name, last name in this block only
  state.apply {
    // state use in here 
    // what if name clash? not as graceful
    hasSeenToast = true
    // not clear scope of this variable, is it part of state? is it part of class? or props?
  }
}

vs ES6 style syntax:
val {firstName, lastName} = this.props
val { isDialogOpen, userHasSeenMessage } = this.state
hasSeenToast = true // clear it comes from class

We can end up with multiple nested apply blocks which is not graceful and could lead to ugly code. Using ES6 style syntax means we keep our code more flat and its perfectly readable. theres no question where these variables come from. In an apply block you might be accessing fields that are within the scope of the apply{} or the scope of the outer block. Its less clear in that sense. Also in the scope of the ES6 style, we can apply different names safely:
val { first_name = firstName, last_name = lastName } = this.props
// first_name, last_name accessible as local vars

7 Likes

For a language that has safety as a big concern, positional destruction (IMO) is not a good design decision. The programmer will need to remember the parameter order always checking the class declaration.

The ES6 version is safer because the compiler can check if you really get the desirable variable.

7 Likes

I think that this was already mentioned that positional destructing is no more unsafe that positional method/constructing invocation. There is a symmetry between being able to do

val person = Person(firstName, lastName)

add

val (firstName, lastName) = person

The later one is just as safe/unsafe (depending on your point of view about safety) than the former one.

That is the same argument that could be used to substantiate the inclusion of name-based destructing (the symmetry argument), but symmetry alone is not enough. Some real use-cases are needed.

1 Like

@elizarov I’m curious, why the destruction by the position was created?
I’m a Kotlin noob, so I curious about the motivation.

And about the name-based destructing, IMO is a good and safe feature.

IMO the code below:

data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
    val person = Person(name="MyName", age=33)
    val (age, name) = person
    println("age=$age, name=$name")
}

Should be transleted to:

data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
    val person = Person(name="MyName", age=33)
    val age = person.age
    val name = person.name
    println("age=$age, name=$name")
}

The position-based destructuring’s primary use-case is to work with mostly structural types like Pair and Triple, so that you can do their construction with val p = Pair(a, b) and destruction with val (a, b) = p which is obviously a better than referring to the Pair’s components with a.first and a.second. It also plays well with Map.Entry and other similar cases.

However, even for nominal types like Person(val name: String, val age: Int) position-based destructuring is just as useful as position-based construction.

Of course, if your style is to construct a person object with Person(name="MyName", age=33), then you should not destruct it positionally as a matter of style. However, if you choose to construct a person with Person("MyName", 33) in your code (that is, positionally), then a positional destructing serves as a nice complement to that.

There is an issue you might be interested in for that matter: https://youtrack.jetbrains.com/issue/KT-14934 to “Enforce parameter usage only in named form”. Now, if a constructor parameter for your Person class was annotated to enforce its usage in a named form only, then it would be logical to forbid a position-based destructucturing for it, too (symmetrically).

2 Likes

So when I wrote that blog post, I will admit I got a little academic in solutions. A really basic thought was to simply stop providing componentN() methods by default for data class, or at least let us opt out. I will give you that it makes 100% sense for Tuple, Pair, heck even List should such a day come. But most data classes by normal non library user are not really intended to be based on position.

On the topic of constructors, I will give you that positional constructor has the exact same problem. The only reason it isn’t as big of a problem is really just our experience, and because every argument is required. We have lived with positional arguments long enough to know not to do that. I don’t think that really makes it a good thing to do.

In your example you show a Person class. I will give you that it looks harmless, but I think we do inexperienced developers a disservice by letting them make mistakes unknowingly. In Person you could easily need to change “name” to “first” and "last. Once again depending on the destructring call, you can have errors.

val (name, _) = personCreatedFromSomeEntirelyDifferentPartOfTheCodeBase // wrong

Heck maybe this is even easier then we think. data class could continue to exist and we just remove class to opt out, use a different name like struct, or something else. I don’t know. The point being, we all like Kotlin more then Java for a lot of reasons, safety being one of them. I don’t want there to ever be a book called “Effective Kotlin” which is basically a book entirely dedicated to informing the user where the language lets you make easy mistakes.

Take for example, Effective Javas Item 9 Always override hashCode when you override equals, Kotlin data classes don’t really have this problem.

Java Concurrency in Practice 3.2.1 Safe Constructor Practices, Kotlin actually helps in this regard by making secondary constructors harder to get to. Granted, it doesn’t fix the problem, but I’ll take it over nothing. (Full disclosure I was an idiot and did this in many programs before I read this book :blush:)

This made me realize that parameter hinting in IntelliJ for positional destructuring would be REALLY useful (and probably somewhat less controversial than replacing it with named destructuring).

6 Likes

A bit late but I just started using Kotlin and in comparison with the many concise language features the current destructuring feature seems inconsistent and error-prone to me. In the Kotlin documentation, the first example for destructuring is:

val (name, age) = person 

That lead me (and possible many other readers) to the expectation that it would be equivalent to:

val (age, name) = person 

Further on the documentation mentions that destructuring is done based on component functions but leaves out what exactly that means for objects. I tried some code to make sure that it behaves as assumed (but not as expected), i.e. evaluating the properties based on the declaration order. And if that order changes, code relying on it can break silently with the possibility of strange side effects.

In the example above, the compiler should be able to generate at least a warning that the order of destructured properties is wrong (based on a comparison of targets to property names). Even better and more intuitive would it be if the assignment would be made from the respective property (with a warning if no matching property exists).

3 Likes