Named-tuple/case class destructuring shorthand

Currently when destructuring case class using named parameters, we have to specify both the parameter name, as well as what we’d like to call the extracted value:

case class User(firstName: String, lastName: String)
val User(firstName = firstName) = User("joe", "shmo") // firstName is now a val with value "joe"

Some other languages that allow similar destructuring offer shortcuts when the extracted variable name is the same as the parameter name, for example in javascript, you can destructure an object like

const user = { firstName: 'joe', lastName: 'shmo' }
const { firstName } = user // firstName is now a const with value 'joe'

or with dart, you can do

final User user = User(firstName: 'joe', lastName: 'shmo');
final User(:firstName) = user // firstName is now a final var with value 'joe'

My suggestion is to bring something like this to scala named-tuple/case class destructuring as well. Not sure what the exact syntax may look like, but perhaps something like dart? Colons are usually used for type ascriptions, but . has been suggested in similar implied field access in this discussion, so maybe we could have something that looks like

val User(.firstName) = user // firstName is now a val with value "joe"
1 Like

I like this idea a lot.

Another possible syntax would be to use braces to imply named arguments, e.g.

val User { firstName } = user

Which would be equivalent to

val User(firstName = firstName) = user

Not sure if that conflicts with any existing syntax.

If we keep it symmetric with the existing named argument syntax then it should be like this:

val User(firstName =) = user

or

val User(=firstName) = user

i guess the second makes more sense as its the RHS that is the pattern - so in this case =firstName means a wildcard placeholder for the firstName parameter

1 Like

I do agree that =paramName is more consistent, but aesthetically I do prefer the dot syntax .paramName. The dot looks more like field access, which I think is closer to what is actually happening than passing a named argument.

EDIT: I must admit the dart example uses : though, which is how values are passed to named parameters

1 Like

I also think the reverse operation, to create rather than destructure instances, could be helpful, where if a variable exists with the same name and type as a case class parameter, we could write something like val user = User(=name, =age) instead of val user = User(name = name, age = age)

4 Likes

What would it take to try to get moving on something like this? A pre-SIP?

1 Like

It would be nice to have the name supplied in all applications.

f(x =, y =)

Usually, the name is longer, so just repeating two or three names makes for a long line.

f(myFirstArg = myFirstArg, anotherArg = anotherArg, etc = etc)

In a recursion, in particular, I intend the existing arg.

I always fall into this syntax when parameters are not distinguished by type; for example, if there is a risk of swapping two Ints or Strings.

I used “trailing equals” syntax, because it’s the other name that is supplied for us.

2 Likes

Should I attempt a pre-SIP then? Would love to keep momentum on this feature going.

this is a pre-sip, just retitle and maybe expand a bit the OP

I don’t like this idea much. It conflates the naming of the parameters at the definition site to the local variables at the usage site. This is a refactor tool nightmare, and bugs waiting to happen.

It’s adding complexity to the language, which is already complex enough. The proposed syntax might conflict with a more important feature we will come up in the future.

In my 20 year software career I never needed a feature that extracts a bunch of variables into local scope. I think it can mostly be avoided by structuring your data better. If you like verbosity that much, I am pretty sure you do not mind also typing the names twice.

What are we optimizing for? So code can be written faster? You should not optimize for code writing speed (especially now with AI). One should optimize for code reading speed, which this barely improves (maybe even worsens due to larger cognitive load).

3 Likes

When i first saw this post (the top one) i thought, yes, sure, it’s foolsday. This will never compile. And then there was a serious discussion …. :thinking:

So i tried it:

case class User(firstName: String, lastName: String)

object Test:
  val User(firstName = firstName) = User("joe", "shmo") // firstName is now a val with value "joe"
  def show(): Unit = println(firstName)

Test.show()

And indeed, this does not compile (3.8.3):

val User(firstName = firstName) = User("joe", "shmo")
                   ^
                   ')' expected, but '=' found

But, when i tried this:

case class User(firstName: String, lastName: String)

object Test:
  val User(firstName,lastName) = User("joe", "shmo") // firstName is now a val with value "joe"
  def show(): Unit = println(firstName)

Test.show()

it neatly shows “joe”. :open_mouth: Really, i was unaware of this “feature”. Nice, never needed it, and i think we should not change it.

1 Like

Note that something very similar to

val User(firstName, name = lastName) = user

already exists with syntax

export user.{firstName, lastName as name}

it’s not a pattern syntax though so the former is still valuable as a subpattern expression

Sure – that’s been true more or less forever. AFAIK it’s exactly the same feature as why you can destructure the case values in match expressions, like:

match person {
  case User(firstName, lastName) => ...
}
1 Like

Aha, yes, that syntax is much used of course. But now i see the problem i missed. It is not that “=” is incorrect, it is just missing the second parameter.

Actually, a better way to enable what author wants is to just use import.

case class X(a: Int, b: String)

def f(x: X): Unit = {
  import x.{a, b}
  println(s"a = $a, b = $b")
}

f(X(10, "c")) // works!

However, I never needed this. In my whole large codebase you will rarely find a function with more than 3 parameters, or class with 5 parameters. You just need to structure your data better.

1 Like

This does actually compile when using 3.8.3 in a project build via sbt locally, and have also confirmed it with scala-cli. There was a bug that was fixed in 3.8.3 itself that allowed it to work, not sure if scastie is behind somehow.

1 Like

Ah, indeed you are correct, thanks for pointing this out. But then, what is left there to improve? If you do not want to use parameter names, you can use this:

val User(firstName,_) = User("joe", "shmo")

and otherwise use the named form, so you do not have to specify all parameters. This feels perfectly natural to me. Use either the named or positional syntax. And if you use the named one, then you should also be specifying the names, even when the parameter and variable names are equal.

Adding yet an other syntax for some minor character reduction gain would confuse more than it brings, imho.

4 Likes

I am happy the sentiment on this proposal shifted. We do not need every typescript / javascript stupidity in our language.

But note, there are still some inconsistencies to fix, see my post: