"Unpacking" classes into method argument lists

@lihaoyi Do you think reusing and extending Selectable as a marker trait for allowing case classes to be unpacked without extra syntax would fit your purposes and be possible? If it can be done then we might be able to have the cake and eat it (i.e. scrap boilerplate and without inventing new syntax).

case class Config(
  name: String = "some app", 
  year: Int, 
  prompt: String = ">"
) extends Selectable  // marks its ability to be selectively unpacked

def foo(year: Int, prompt: String) = ???

def bar(cfg: Config) = foo(cfg)  // partially apply selected fields of cfg

bar(Config(year=1984))  // normal call

bar(year = 1)  // cfg is packed with selected default args

I agree; that is certainly a value from this proposal.

Python has a very different context than Scala. They had the kwargs dict for a long time. It is somewhat of a standard approach at this point. It doesn’t call any constructors. It is just a syntactic sugar for passing a dict. In Scala, the unpack, however, will appear in a kind of void. Maybe if we had something similar to the kwargs-like dicts, we could work with that.

I know that it is similar to what some people here were proposing, but I think we could try to combine it by implementing these steps:

Named tuples

Named tuples could be a de facto type-safe substitute for dynamic dicts in Python. So let’s say we have a named tuple like this:

type Foo = (a: Int, b: String)

Unpacking case classes

To make it possible to use case classes like Config in this solution, we can introduce a parameterized type alias special-cased by the compiler - Unpack. Unpack would take a T <: Product and, when possible, return a named tuple with its elements.

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

type Foo = Unpack[Config] // Foo = (a: Int, b: String)

That way, we don’t have to use the Selectable that I’d argue is meant to serve a bit different purpose (cc @prolativ)

Spreading named tuples in parameter lists

After one has obtained the named tuple, he would be able to spread it using a spread operator (like ..)

def foo(bar: Int, ..conf: Unpack[Config]) = ???

Spreading named tuples as values in the call site

To accommodate for easy transformation back to the Config object, we could spread the named tuple at the call site:

def foo(bar: Int, ..conf: Unpack[Config]) = 
 val conf = Config(..conf)
 ???

I think that this is an approach that solves the problem that Python solved with their PEP. It doesn’t call any constructors, and would serve just as a syntactic sugar to skip the unnecessary parentheses and sometimes to be able to pass the params of an unpacked Product without specifying them one by one.

2 Likes

Why is the … syntax even needed? Isn’t it enough that it is marked Unpack ?

Unpack would just go from Product to Named Tuple. .. would spread the Named tuple. Basically, just like Python does - it separately unpacks the type if it is not a TypedDict (with Unpack), and separately spreads it with ..

But couldnt that be done automatically, like autountupling:

def foo(bar: Int, conf: Unpack[Config]) = 
 val conf = Config(conf)
 ???

No .. needed similar to autountupling?

1 Like

This seems like a perfect use case for named tuples. Having those would also allow us to do many more things than this narrow application.

The definition site would need a way to distinguish a single named tuple parameter from an unpacked version of said parameter, however at the use site val conf = , that is pretty unambiguous and auto untupling could be implemented there.

2 Likes

I just wanted to put a vote for a simpler scala, and I don’t think we have argued why this is worth while.

The argument of “python did it” seems to be the most commonly cited, but actually what python did was make a way to type annotate an existing pattern of code.

In my opinion, this adds very little. The bar should be high to change the language. We should see many very compelling examples before we tax the community and compiler development with an entirely new way of defining parameters to methods. Again, python has had keyword arguments for a long time due to the dynamic patterns quite common there. We don’t have such dynamic patterns very much in scala. Indeed, when people have suggested Selectable here (which is much closer to python’s keyword argument use case) it seems to be getting no traction.

6 Likes

If there is a full match between args and the named tuple - why not. Spread would be needed when NamedTuple is only a part of the arguments.

1 Like

But if only parts of params are used, the compiler could autmatically match an pick whats unambigously applyable, similar to default args today?

But isn’t Selectable very similar, given the connection to named tuples that was proposed above by @julienrf ?

Here’s another use case:

In scala-js-react applications, you typically define a component’s props as a case class. To instantiate the component you pass it an instance of the props case class, but it’s more idiomatic to pass the parameters so often one writes an apply method to create that sugar, but it sounds like unpack would make that automatic.

Although you’d still have to have an apply method somewhere. Could the definition work with an abstract type?

Anyway, as much as I’d benefit from this, as I think I’ve said before, whatever bandwidth exists to develop this feature would be much better spent getting IntelliJ to Scala 3 feature parity or other tooling pain points. I don’t think we should be adding these kinds of features before tooling is up to speed with the existing features.

2 Likes

I agree with this sentiment as well a lot, but on the subject of use cases: in the realm of UI development (which you could argue scala barely exists) tons of parameters for every type is the norm. Because of the commonality between components and their parameters, you are left with inheritance and setter methods (because even generating builders is too much work).

Being able to define parameters as extensible records (so you can ++ them together like tuples) and being able to take packed arguments of them in methods/constructors can really make or break UI coding in scala. This is true for scala.js, scala.native, scala+swing, scala+javafx, scala+qt.

4 Likes

That might be putting the cart before the horse. There is no intersection between the people who can improve IntelliJ and the people who could work on this proposal. If you want a better IntelliJ, you need to lobby Jetbrains to resource it better. And the best argument you’d have is that Scala is a language that gains new traction because of its user-friendly features.

4 Likes

Interestingly, some kind of named parameter packing is already achievable (at least since scala 3.1.0), although this seems quite hacky and fragile, e.g.

case class Point(x: Int, y: Int)

def foo(point: Point) = ???

extension (t2: Tuple2.type)
  def apply(x: Int, y: Int) = Point(x, y)

val foo1 = foo(Point(x = 1, y = 1))
val foo2 = foo(x = 1, y = 1)

The problems/limitations of this approach that I can think of at the moment:

  • At least 2 named parameters have to be passed to the invocation of foo
  • There cannot be other parameters in the same parameters list besides the unpacked one
  • One has to somehow bring the extension method into scope (maybe context functions could be helpful here but that would slightly clutter the signature of foo, e.g. def foo(point: Unpack[Point] ?=> Point)
  • The extension method has to be available on the companion object of a tuple type with arity matching the number of actually provided named parameters, which might make things tricky, especially if one wants to have default parameters, e.g.
case class Point(x: Int, y: Int, z: Int)

def foo(point: Point) = ???

extension (t: Tuple2.type | Tuple3.type)
  def apply(x: Int = 0, y: Int = 0, z: Int = 0) = Point(x, y, z)

val foo1 = foo(x = 1, y = 1)
val foo2 = foo(x = 1, y = 1, z = 1)

– in this case just extension (t2: Tuple2.type) or extension (t3: Tuple3.type) wouldn’t work

  • This pattern seems to rely on autotupling, even though strangely this won’t work if one writes the additional parentheses explicitly like foo((x = 1, y = 1))
  • Compiler error messages are totally unhelpful if there’s a typo in the name of any of the parameters
  • IDEs don’t help either, although this could be improved
3 Likes

I don’t want to derail the thread but could you elaborate a bit?

Why is that? The Scala plugin, and the Community Edition of the platform, are OSS.

I didn’t understand what you meant here.

Does someone want to file a SIP along the lines of the MVP? I think I would be supportive.

It turns out, the latter solves the former !

class Seq[T] private (contents: ???):
  def this() =
     Seq(contents = empty)
  def this(h: T, unpack tail: Seq[T]) =
     Seq(contents = h +: tail.contents) // TODO: optimize performance

I don’t know if I find it delightful or terrifying, but it should work !

Example:

Seq(1, 2, 3)
// desugars as
Seq(1, Seq(2, Seq(3, Seq())))

I’ll pick a nit briefly: this is packing, not unpacking. It takes ingest(1, 2, 3) and packs the arguments into a Seq.

Unpacking would be:

def ingest(unpack one: Int, two: Int, three: Int)

ingest(Seq(1,2,3))

From the perspective of the caller, the function is saying it will unpack the argument. And from the perspective of the function author it is saying we want to support unpacking an object into constituent parts for our internal use.

2 Likes

I like to think of this as optionally unpacking the type into parameters (or flatmapping since it replaces the original parameter). In which case you could make an argument about its location.

That is why having a type like Unpack[_] being able to provide a theoretical named tuple (or record) type to use as actual parameters would make a lot of sense here. Although that might complicate recursive application, which seems a little more straightforward with the currently described system.

I can write something up, but I’m totally unfamiliar with formal specifications and compiler internals so I would need support in those regards

3 Likes