[Pitch] Introduce Expanded Parameters

You can keep the default memberwise initializer by writing other initializers in an extension.

extension ColorfulBox {
    init(colorlessBox: ColorlessBox, color: Color) {
        width = colorlessBox.width
        height = colorlessBox.height
        depth = colorlessBox.depth
        self.color = color
    }
}

You don’t need anything in this proposal to address your complaints.

2 Likes

Thanks, somebody else pointed this out earlier in the thread. Admittedly, it slips my mind as I tend to avoid reaching for extensions when I’m defining a type.

Also, (as mentioned upthread) there is a more subtle case where you don’t want the generated memberwise init, but you do want an init with the same args, as well as an init with a related type, and thus you’d avoid writing one init with the proposed feature.

:100:

despite being totally nonscientific, this table approximates languages' complexities quite well! with swift close to top already :astonished:

Nonscientific, not obviously correlated with practical complexity, and also factually wrong.

4 Likes

i'd like to see the languages' practical complexity table to compare it with the quoted nonscientific one to see how far the two are off. please share the link if you know one.

I'm mixed on this this feature. I personally would love to use it when I have enums with a single associated struct value ex:

enum State {
  case initial
  case open(Open)
  case closed(Closed)
  // ...
  struct Open { ... }
  // ...
}

@expanded would be wonderful for avoiding .init() on every use and allow for more natural construction. However, I think the proposed solution is too broad and allows the attribute to be used in many situation that would confuse users (and me).

4 Likes

I appreciate the motivation for a feature like this. It can be somewhat tedious to splat out parameters to be used for convenience invocations like the ones mentioned in the OP.

From the API crafter's POV, I'm not sure I'd want this to cross the module boundary though. @expanded would be fine for internal expansion of parameters for convenience, but I don't think it should get applied to public/exported methods. Public-facing APIs from a module should be carefully, deliberately, and consciously chosen to balance expressivity without overly bloating the surface area. Public APIs have on-going costs in terms of maintenance, source-stability, ABI-stability, etc and I worry that something like this would make it too easy to expose too much such that it hinders library authors in the future.

4 Likes

In my opinion, this isn't the only problem. A combinatorial explosion of overloads is annoying for library authors, but it also has a negative effect on ergonomics of using the library. Searching through these overloads to find the right combination of parameters takes time. Overloads cause the same issue that folks are surfacing about this feature, which is that it makes it much harder for readers to determine which function is being called and how to find its definition. Overloads also come with extra costs in compile time and code size. This proposal is allowing a common library pattern to be expressed using a tool other than overloading, because these things don't really need to be separate overloads.

I understand your point about a plethora of separate attributes, but I also think the targeted code generation features that Swift has today are more approachable than a general macro system because they are so limited in how the code can be transformed. Constructs that a user sees, e.g. an if-statement or a function call, are never going to have drastically different behavior than what the user expects because a library can't intercept the basic functionality and transform it to do something else. The feature that has the most drastic transformation -- result builders -- clearly doesn't look like normal Swift code, and that's an intentional aspect of the design (I think, I am not one of the designers of that feature :slightly_smiling_face: ).

5 Likes

This sounds fitting for a particular case I've been bumping into with a geometry library I'm working on that has lots of generics going on, where I specify methods using generic Vector types, but then have to manually add convenience calls with coordinates by copy-pasting definitions around, for both x/y and x/y/z coordinates, which I have to manually cover with unit tests, too. Will be following the pitch thread as it unfolds!

I've updated the proposal on GitHub. TL;DR of the changes:

  • Limit expanded to the first parameter only;
  • Limit initializers - only the ones available at the function definition;
  • Limit overloading functions that use @expanded;
  • Add closures to the scope;
  • Remove the repeated label limitation (thanks @Zollerboy1 for the suggestion)
6 Likes

I think this pitch has the potential to offer significant benefits to API clients, not just authors. Composing initializers using @expanded provides strictly more information to the tooling versus providing the same set of explicit overloads.

As an example, an autocomplete system could provide hierarchy in its suggestions. Instead of a flat list of all the overloads for an initializer call, the system could offer the smaller set of explicit overloads, then offer a second list of initializers to fill the expanded parameters. Instead of an exponential explosion of initializers, we get a logarithmic reduction is search space.

The tooling could provide similar affordances for accessing the documentation or type information for an existing initializer call when reading source code.

I understand that specific tooling changes are out-of-scope for Evolution (modulo LSP?), but the additional information provided by @expanded is still a point in its favor.

1 Like

Right, but it seems like this is what the proposal is offering, either way. Whether it's an "overload" or an "expanded parameter" doesn't seem to make much difference as far ergonomics or legibility is concerned.

I mean, sure (I agree), but result builders already do all of that. They change the meaning of if statements, of constructing a variable, of... well, basically everything. And in a way, that's sort of by design - they are used for DSLs; languages which exist within Swift but are not Swift.

What I'm suggesting is more moderate: for a code generation feature which takes Swift syntax as input (say, a list of initializers) and outputs Swift code based on that input (say, a bunch of overloads which repeat the arguments of those initializers and forward to them), with the transformation also being written in Swift, alongside the code which uses it.

Things like this are already possible using plugins. For instance, the distributed actors sample app uses swift-syntax to parse a package target's code and generate new declarations based on that. I think we should add those capabilities in to the language itself somehow (perhaps including swift-syntax as a core library?), but in any case - it's doable, and the transformation doesn't need to live in the compiler or become part of the language.

Is there any reason why @expanded parameters couldn't be implemented using a plugin to generate overloads (perhaps using some kind of parseable comment, e.g. // expand-argument:gradient)?

1 Like

Did I miss anything, or does the proposal say something about its primary motivation which is related to property wrappers?

I don't see much really valuable here. It's more difficult to understand and navigate in the code, to access documentation of written code. What are IDE supposed to do in order to let users interact with method and initializer definitions???

I'm not happy with a narrow proposal that, as pitched, focuses on allowing cheap "better-looking" API.

3 Likes

I'm not familiar with macros and the concept of adding code generation to a language (TIL!). Setting that aside for a moment, this suggestion sounds like another approach one could think of when considering this feature. However, we would only get the benefit of not having to type out the overloads (since they would be generated from a set of inputs). The benefits to readers of code using @expanded (leaner APIs to reason about) and to the compiler (when resolving overloads) are gone.

I found out about this feature in a discussion of the previous pitch I worked on (related to property wrappers). However, it isn't a primary motivation for @expanded (property wrappers aren't even mentioned in the proposal).

Difficulty to navigate code/documentation is already present when using overloads to cover different initialization methods. When considering an overloaded API like the ones mentioned in the proposal and this thread, the fact that those different parameters will lead to the initialization of a value with the same underlying type is hidden from the user. With expanded, this underlying initialization is explicit and I believe it makes code easier to understand.

@curt's suggestion on how tooling could support this feature is quite interesting.

an autocomplete system could provide hierarchy in its suggestions. Instead of a flat list of all the overloads for an initializer call, the system could offer the smaller set of explicit overloads, then offer a second list of initializers to fill the expanded parameters. Instead of an exponential explosion of initializers, we get a logarithmic reduction is search space.


While I'm not familiar with the concept of splat/splatting (is it similar to what the expanded attribute is aiming for?), the ExpressibleByTupleLiteral part is interesting. It seems like a feature of its own though, since it would affect other aspects of the language such as initialization patterns. Also, to make use of the information that a given type is ExpressibleByTupleLiteral when matching arguments to parameters would require digging into the type (I might be wrong here). Type analysis was mentioned earlier in the thread by Holly as something to be avoided, so we don't want to make the compiler try to resolve the parameter type to pick its arguments.

I updated the proposal to not allow methods with expanded parameters to be overloaded. Also, perhaps @filip-sakel's suggestion to name the attribute expandable would better communicate that it's also possible to pass the non-expanded form.

2 Likes

Property wrappers are mentioned, in the first sentence of this thread, as the initial motivation of this pitch:

It would be good to tell if expanded parameters are a necessary step towards sharing property wrapper storage. People who want shared storage, but do not like this proposal, could change their mind, or suggest alternative ways to reach the same goal.


Autocomplete is indeed part of the tooling of developers who interact with code. Another part is navigating to initializer and method definitions, and yet another is displaying documentation of initializer and method definitions. Both help accessing important information.


I'm not sure I understand what you mean. Each overload would have one location, and IDE can jump right to it.

And even if this were true, what about removing difficulty, instead of adding some? Designing APIs for screenshots instead of developer experience sounds ill-advised to me. When I say developer, I think of the end users of APIs, not the API designers.

This pitch is not balanced. It favors API designers over end users.

When considering an overloaded API like the ones mentioned in the proposal and this thread, the fact that those different parameters will lead to the initialization of a value with the same underlying type is hidden from the user. With expanded, this underlying initialization is explicit and I believe it makes code easier to understand.

Again, I'm not sure I understand what you mean. Nothing tells me, when reading code, if I'm looking at an overload or an expanded parameter.

And again, this assumes that overloads are the correct API with the current Swift, and that the language needs a new feature in order to help people who design such APIs (note: IMHO, it helps those people, but not the end-user developers, and I wish the initial need was questioned in the first place).

1 Like

The sentence you quoted was intended to give a little bit of background to where this proposal came from. When I said property wrappers aren't a primary motivation or mentioned in the proposal, I was literally referring to the motivation section.

Anyway, I don't think this feature is required for shared storage.

Consider the journey of some developers trying to decide how to use such API. First, the documentation has many overloads, they would have to decide which one to use. Perhaps jump from one method to the other and eventually realize that the only thing they do differently is the underlying Gradient initialization. Using expanded instead, there would be one single method with an explicit dependency on Gradient right there on the signature. The decision path is simpler.

Now, to someone encountering a method call that happens to use expanded. They can jump to the definition and notice the arguments at the call site don't align with the signature. But also, there's an attribute that is meant to change arguments. From there, it's possible to conclude the strange arguments are from an initializer - since expanded doesn't allow factory methods, etc.

This was one of the main pieces of feedback from this thread! Thanks to it I'm working on improving the proposal to better reflect the benefits expanded offers to all users.

5 Likes

Yes, I understand. That's fair.

Thanks for that. You have understood that not everybody think that overloads or "enhanced overloads with expanded parameters" are good api. Baking one more ad-hoc feature in the language, in order to support a fashion in API design, is a trend that, as one should expect, meets resistance. This pitch is about a costly aesthetic preference.

This question should be addressed, so that the debate about fashion/aesthetic vs. language complexity can also take place.

I definitely side with those that think this is a pretty undiscoverable and far reaching solution to a very niche problem, but how about this.

The proposal wants to simplify this

struct Point {
    let x
    let y
}
struct Square {
    let centre: Point
    let side: CGFloat
}
let oldSquare = Square(centre: init(x: 1, y: 2), side: 10)

into this:

extension Square {
    init(centre: @expandable Point, side: CGFloat) { ... }
}
let proposedSquare = Square(x: 1, y: 2, side: 10)

Apart from what has been mentioned, I also think it's a problem that the argument labels for the inner initialiser might not make sense for the outer one. In this case it's sort of OK, but you can easily imagine cases where it's very confusing.

So my proposal is to leverage my favourite non-existing feature, TupleRepresentable:

extension Point: TupleRepresentable { }

let bestSquare = Square(centre: (1, 2), side: 10)

The benefits are:

  • We need this feature anyway!
  • It will be up to the inner type to determine if it can be represented by a tuple or not
  • Important: We don't flatten the argument list, the two centre arguments are kept together
  • You keep the argument label of the outer type, and since the inner type guarantees that it makes sense to replace it with a tuple, the combination will make sense
6 Likes
Terms of Service

Privacy Policy

Cookie Policy