"Unpacking" classes into method argument lists

Mulling this over after discussing with @sjrd and some others at lunch today, I now think this can really be super simple. Here is a MVP (meaning minimum viable proposal).

Unpack modifier

Introduce a new unpack modifier that can be added to the last parameter of a method. Example:

case class Config(size: Int, label: String = "")
def request(body: Body, unpack config: Config) = ...

The type of an unpack parameter must be a statically accessible class. Inside the method, unpack has no significance; we simply have a parameter of the declared type.

When such a method is called, we first match arguments as usual. When it comes to matching the last formal parameter, we simply wrap the remaining actual arguments with the apply method of the class. Example:

request(body, 20, label = "abc")

would expand to

request(body, Config(20, label = "abc"))

The expansion is done before typing. So there could be several apply methods and overloading resolution will pick the correct one.

Reuse Spread Operator

If we have a call like

request(body, config*)

we pass the last argument “as is” without wrapping. This works in the same way for repeated parameters (where the argument must be a sequence or an array) and for unpack parameters, where the argument must simply match the formal parameter type.

Notes

  1. The unpack parameter class will often be a case class (since then it is easy to pick it apart) but this is no absolute requirement. In Scala 3, regular classes are provided with synthetic apply methods as well, and we can use them to wrap the arguments.

  2. As always, the unpack class can itself have default arguments which then translate to default arguments of the unpacked version. So with the definitions above

    request(body, size = 20)
    

    would be legal and expand to

    request(body, Config(size = 20))
    

    which in turn expands to

    request(body, Config(size = 20, label = ""))
    
  3. The Config class can also have defaults for all parameters in which case we can leave out the argument completely. So given

    case class Config(size: Int = 20, label: String = "abc")
    

    we can write

    request(body)
    

    and this would expand to

    request(body, Config())
    
  4. I believe the question whether we want to accommodate multiple unpack parameters per method and/or multiple spread operators per class is orthogonal. This would probably raise a lot of tricky questions of how to expand and disambiguate. This proposal intentionally leaves that out. I would claim we thus get most of the benefit with very little additional language complication. But it could well be added later.

  5. The proposal also intentionally does not allow you to use a spread operator to expand into multiple normal parameters of the called method. I believe such a feature would lead to too clever-by-half code which could become very confusing.

  6. Interestingly the question of special named arguments turned out to be a red herring. The proposal works with named and positional arguments in the same way.

8 Likes