[Pitch] Introduce Expanded Parameters

Hey everyone! A few weeks ago I pitched Shared Storage for Property Wrappers, and TL;DR it was restructured into 3 other features. One of them, @expanded parameters, is what I'd like to get feedback on today. The updated proposal is on GitHub.


Introduction

Swift is a language that allows us to write expressive API interfaces. With features like constrained Generics, method overloading, trailing closures, and default arguments, you can reduce code duplication while still achieving quite flexible APIs. This proposal aims to increment this part of the language with @expanded, a new attribute for function parameters.

Motivation

Let's start with an example. SwiftUI has great APIs for styling shapes with gradients. For instance, you can fill a Rectangle with a linear gradient by passing an array of colors, startPoint, and endPoint to the linearGradient() static function:

Rectangle()
  .fill(
    .linearGradient(
      colors: [.yellow, .teal],
      startPoint: .init(x: 0.5, y: 0),
      endPoint: .init(x: 0.5, y: 0.6)
    )
)

Because it's possible to create a Gradient with an array of Gradient.Stop, this API also lets you pass an array of them instead. Or you can pass a Gradient value directly. For each of these options, there's an overload of the linearGradient method.

// A linear gradient.
static func linearGradient(Gradient, startPoint: UnitPoint, endPoint: UnitPoint) -> LinearGradient

// A linear gradient defined by a collection of colors.
static func linearGradient(colors: [Color], startPoint: UnitPoint, endPoint: UnitPoint) -> LinearGradient

// A linear gradient defined by a collection of color stops.
static func linearGradient(stops: [Gradient.Stop], startPoint: UnitPoint, endPoint: UnitPoint) -> LinearGradient

This API has one "original" function that takes a Gradient directly, and two "convenience" overloads for each way a Gradient can be created.

I don't know how these are actually implemented inside SwiftUI, but for this proposal let's assume these methods will create a Gradient somewhere down the line and apply it to a shape, with only the initialization method for the Gradient value differing between them.

So what if we were to add a third initializer to Gradient?

extension Gradient {
  init(materials: [Material]) { ... } 
}

In that case, we might want to add the equivalent linearGradient overload method to keep our API consistent.

// A linear gradient defined by a collection of materials.
static func linearGradient(materials: [Material], startPoint: UnitPoint, endPoint: UnitPoint) -> LinearGradient

Given this, the potential of an overload "explosion" is already a problem. But it gets worse considering this pattern can spread out fairly quickly ā€” now there's an entire family of gradients that could be updated. Radial, angular, and elliptical, all with their respective helper methods, would be good candidates for adding an overload with a materials parameter to support the new initializer.

// A radial gradient.
static func radialGradient(Gradient, center: UnitPoint, startRadius: CGFloat, endRadius: CGFloat) -> RadialGradient

// A radial gradient defined by a collection of colors.
static func radialGradient(colors: [Color], center: UnitPoint, startRadius: CGFloat, endRadius: CGFloat) -> RadialGradient

// A radial gradient defined by a collection of color stops.
static func radialGradient(stops: [Gradient.Stop], center: UnitPoint, startRadius: CGFloat, endRadius: CGFloat) -> RadialGradient

// A radial gradient defined by a collection of materials.
static func radialGradient(materials: [Material], center: UnitPoint, startRadius: CGFloat, endRadius: CGFloat) -> RadialGradient

Writing flexible APIs shouldn't always come at the cost of code duplication, and that's the problem @expanded aims to solve.

Proposed solution

We propose a new type attribute that allows function parameters to be fulfilled with the arguments of an initializer instead. So we can write a new version of linearGradient that takes an @expanded Gradient.

static func linearGradient(_ gradient: @expanded Gradient, startPoint: UnitPoint, endPoint: UnitPoint) -> LinearGradient { // do gradient stuff ... } 

At the call site, the gradient parameter can be fulfilled with the arguments for Gradient initializers, so all of these are legal:

// call it with an array of Materials
linearGradient(materials: [.thinMaterial, .thickMaterial], startPoint: 0.1, endPoint: 0.5)

// or an array of Colors
linearGradient(colors: [.yellow, .teal], startPoint: 0.1, endPoint: 0.5)

// or even these Gradient.Stop
linearGradient(stops: [.init(color: .blue, location: 12)], startPoint: 0.1, endPoint: 0.5)

It eliminates the need to add several overloads to support the different ways a parameter's value can be initialized. It's also fine to call it the old "non-expanded" way:

let myGradient = Gradient(colors: [.yellow, .teal])

// non-expanded call.
linearGradient(myGradient, startPoint: 0.1, endPoint: 0.5)

Detailed design

@expanded (bike-shedding is welcome here) is a Type Attribute and can be used in functions and subscripts parameters. This allows API authors to be in control of which parameters would support this functionality at the call site.

When given arguments with labels that don't match the original one, the compiler will use overload resolution to select an initializer.

class Pastry {
  init(a: String) { print("initializer A chosen") }
  init(b: Bool) { print("initializer B chosen" }
}

func bake(pastry: @expanded Pastry) {  }
bake(a: "cake") // prints initializer A chosen

extension Pastry {
  init(pastry: Bool) { print("initializer C chosen") }
}

bake(pastry: true) // error, the compiler expects a Pastry instance.

This attribute works by introducing new rules to argument-parameter matching in the compiler, and it places a few limitations on which parameters can be next to an @expanded.

Argument-parameter matching

This is the process of pairing parameters of the function declaration with their respective arguments. It iterates on the declaration taking one parameter at a time and looking for an argument with the same label. At this point, only labels are relevant for matching ā€” types are not part of the example.

To illustrate that, let's look at how argument-parameter matching works for the function test.

class Example {
  init(name: String, count: Int) { }
  init(count: Int) { }
}

func test(first: Bool, second: @expanded Example, third: Int) { }
test(first: true, name: "A", count: 2, third: 10)

The first iteration will take the 'first' parameter and match it to the argument with the same label.

The second iteration is the one that matters for this example. It will take the 'second' parameter and look for an argument with the same label, just like in the previous step. If it finds one, then this parameter will be treated as any other. Now, if it doesn't find it, we'll do some extra fun things since this is an @expanded parameter.

The compiler will first look at the label of the following parameter if there is one. In this case, the label is 'third'. After that, we'll start taking the subsequent arguments that don't match the label of the next parameter and use them to build the "expanded initializer" call. We stop when we find an argument with a label that matches 'third'.

This way, 'name' and 'count' are the arguments that will end up being used for the expanded initializer. Using these arguments the compiler will select the appropriate Example init.

After properly matching 'second', we match the 'third' parameter to its argument, and we're done.

This is a simplification to convey why the parameter next to @expanded is so important. The limitations mentioned in the following sections are a consequence of that.

Repeated parameter labels

When a label appears in an initializer of the expanded type and the function with the expanded parameter, the rules described above may lead to rather unexpected behavior. So in the following example, even though the arguments at call site seem right, the compiler won't collect the 'emoji' argument for the expanded call, since its label matches the parameter next to @expanded.

struct Pastry {
  init(named: String, emoji: String) {}
}

func bake(pastry: @expanded Pastry, emoji: String) {}
bake(named: "pain au chocolat", emoji: "šŸ„+šŸ«", emoji: "šŸ¤Ø")

Multiple expanded parameters

API Authors can choose one, or many, parameters to allow expanding in the same function. In the case of many expanded parameters, there's a limitation to be aware of: two expanded parameters aren't allowed to be next to each other.

// not okay
func testExpanded(myClass: @expanded MyClass, myClass2: @expanded MyClass) { }

// okay
func testExpanded(myClass: @expanded MyClass, c: Int, myClass2: @expanded MyClass) { }

Default arguments

@expanded parameters can't be immediately followed by a parameter with a default argument. Unless this parameter is the last one. An error will be thrown, suggesting you move the parameter with the default argument to the end of the function.

// not allowed
func testExpanded(myClass: @expanded MyClass, b: Int = 10, c: Bool) { }

// okay
func testExpanded(c: Bool, myClass: @expanded MyClass, b: Int = 10) { }
func testExpanded(myClass: @expanded MyClass, c: Bool, b: Int = 10) { }

The @expanded parameter itself can have a default value.

extension MyClass {
  static func test() -> MyClass { } 
}

func testExpanded(a: @expanded MyClass = .test()) { }

Limitations on the initializers that can work with @expanded

ā€¢ Initializers with unlabeled arguments
Initializers need to have at least one parameter with a label to work with the expanded feature. Inits with a single unlabeled parameter aren't allowed.

class C {
  init(_ a: Int) { } // not allowed
}

ā€¢ Initializers with trailing closures
When a given expanded type has initializers with closures, the trailing closure syntax can't be used. The expanded expression is limited by the bounds of the "original" parenthesis.

class FancyClass {
  init(a: Int, _ b: () -> Void) { }
}

func test(one: @expanded FancyClass) {}
test(a: 7, { print("šŸ¦†") })  // okay

func testAgain(one: @expanded FancyClass, two: Bool) {} 
testAgain(a: 7, { print("šŸ¦†") }, two: true) // okay

Nominal types

Since @expanded expands an argument into its initializer call, it can only be used with types that can have initializers. The compiler will enforce it by checking if the type to which this attribute is attached is a Nominal Type (aka a type with a name, not a structural type).

func testExpanded(a: @expanded () -> Bool) {} // error
func testExpanded(a: @expanded (Bool, Bool)) {} // error

Access control works as usual for expanded parameters. If a certain initializer isn't visible from the call site of the expanded method, it can't be used.

public class Duck { 
  let named: String
  private init(named: String) { }
}

func pet(a: @expanded Duck) { }
pet(named: "Maria") // error

Subclassing

The compiler expects arguments to build an initializer call of the parameter type. Therefore, subclassing doesn't work with this feature.

class SuperClassy {
  init(one: Bool, two: Int) {}
}

class SimpleClass: SuperClassy {
  init(a: Int, b: Int) {}
}

func test(classy: @expanded SuperClassy) {}
test(a: 10, b: 10) // error, "a" and "b" are arguments of the "SimpleClass" initializer. 

Protocols

This feature can't be used with Protocols ā€“ even if the protocol has an initializer requirement. The compiler needs to know which concrete type to instantiate.

protocol MyProtocol {
  init(a: Int)
}

struct SimpleStruct: MyProtocol {
  init(a: Int) {}
}

func test(x: @expanded MyProtocol) {}
test(a: 10) // error

The example above would be the equivalent of trying to construct a protocol type test(x: MyProtocol(a: 10)).

Generics aren't part of the scope at this moment.

Inout

inout and @expanded cannot be used together since the expanded call constructs a new value, and inout relies on modifying an existing one.

Impact on existing code

This feature introduces the opportunity for a lot of APIs to be refactored. Adding @expanded to an ABI-public function parameter isn't a source-breaking change. But since it won't emit overloads to the ABI, adding @expanded doesn't allow the deletion of obsolete overloads either.

Alternatives considered

Future directions

Add support for enums to use @expanded parameters.

Optionals and failable initializers

When dealing with optional expanded parameters, it's unclear whether the appropriate behavior is to build an initializer call to the Wrapped type or the Optional type itself. Therefore when an optional type is given to an expanded parameter, the compiler will try to build the desugared type.

class SimpleClass {
  init?(a: Int) { }
  init(a: Int, b: Int) { }
}

func test3(x: @expanded SimpleClass?) {}
test3(a: 10) // error
test3(a: 10, b: 20) // error

In the example above the compiler will try to build Optional<SimpleClass>. In the future, this behavior can be explored to allow unwrapping the optional.

Expanded trailing closures

In the future, this feature could be expanded (:yawning_face:) to allow trailing closure syntax to be used when the expanded parameter is the last one.

class FancyClass {
  init(a: Int, _ b: () -> Void) { }
}

func test(one: @expanded FancyClass) {} // trailing closure syntax allowed
test(a: 7) { 
  print("šŸ¦†") 
 }

func testAgain(one: @expanded FancyClass, two: Bool) {} // not allowed
testAgain(a: 7, { print("šŸ¦†") }, two: true) 
23 Likes

Taking this an example, my first reaction is that I donā€™t see how this is significantly better than just requiring the caller provide an instance of Gradient:

linearGradient(.init(materials: [.thinMaterial, .thickMaterial]), startPoint: 0.1, endPoint: 0.5)

The restrictions that youā€™re having to place on the usage of this proposal (two @expanded parameters canā€™t be adjacent to each other, parameters with default arguments canā€™t immediately follow an @expanded parameter, etc.) worry me, and are going to cause some extra mental overhead and limitations in using this feature, for - in my opinion - a pretty minor source-level simplification.

24 Likes

A few questions:

This behavior doesn't make sense to me. The compiler can already handle multiple external labels which are the same, why can't the expanded synthesis do it too, especially when the values are just turned into a Pastry value and passed to the function?

Can you expand on why this is? Is it a parsing issue or worry about user ambiguity? I'm generally okay with the rule, as long as there are good diagnostics, and putting defaulted parameters after non-defaulted is the suggested style already, but it seems an odd edge.

Again, any particular reason why? I can see the ambiguity with multiple unlabeled initializers, but if the expanded type only has one, what's the issue?

Does this include tuples with labeled parameters? If so, adding it to the example might clarify things. And some reasoning might would be appreciated here, especially for tuples. Even if users can't write their type name, surely the compiler can?

Overall, this pitch is very interesting, but the sheer number of edges that are already visible is rather worrisome. I'd hate to see Swift adopt another feature that works fairly well for Apple's anticipated use cases but doesn't for the general community.

17 Likes

I'm afraid I don't understand the example. Isn't it simpler to use factory functions instead of adding new initializers?

extension Gradient {
  static func materials(...) -> Gradient
  static func colors(...) -> Gradient
  static func stops(...) -> Gradient
}

linearGradient(.materials(...), ...)

The example is using an initializer, but that seems to work against it, because there isn't a simple way to say "pass all the arguments from this call-site to this other function" in Swift (unlike C++, where you could use variadic templates + optionally std::forward to do this kind of thing). So why not avoid the initializer in the first place?

4 Likes

This behavior is due to the order in which the argument/labels are parsed. First, we collect arguments based on them not matching the parameter next to the expanded one. Then after that, we resolve the initialization of Pastry by looking up its initializers. To allow this scenario we would have to reverse it, looking up the initializers for Pastry before collecting the arguments.

It is mainly due to how argument/parameter matching is implemented. And since the parameter next to @expanded is important to decide when to stop collecting arguments for the expanded call, so is the corresponding argument.

Preventing ambiguity and also widespread implicit conversions.

Thanks for pointing this up! I'll investigate this scenario since I didn't consider tuples with labeled parameters up until now.

All of these overloads just return an instance of LinearGradient. As far as I can tell, the only reason to have these static methods at all is so Xcode can suggest them when you type . where a ShapeStyle is required. Maybe a better solution (for such scenarios) is for Xcode to suggest all the constructors of types conforming to ShapeStyle, and automatically delete the . if you pick one.

Argument-to-parameter matching in Swift is done based solely on argument labels. The type of the argument is not considered when binding an argument to a parameter. For example:

func test(label a: Int = 0, label b: String = "") {}

test(label: "") // error: Cannot convert value of type 'String' to expected argument type 'Int'

It's crucial to maintain that property, which is why there are limitations on the parameter following an @expanded parameter. The compiler needs to decide which arguments are "expanded" without analyzing the argument and parameter types (which are not necessarily known at the time when argument-to-parameter matching is done).

4 Likes

This is a viable alternative, though (and it's actually the approach we started with!). That said, there would still need to be some rule for deciding where one expanded argument list ends and the next one begins, which would also come with some restrictions. It would still encourage the argument labels to be somewhat disjoint between two adjacent expanded argument lists, similar to how this proposal encourages disjoint argument labels between the expanded argument list and the rest of the argument list. This alternative strategy would also never allow you expand an argument with a generic argument type, although that's arguably not-that-useful because the generic argument would need to be inferred from elsewhere (or explicitly specified), but it's a limitation nonetheless.

I also don't know how much these limitations will really matter in practice; it might be good to gather more use cases into the proposal to do a mini-analysis of these limitations. For example, the default argument limitation I illustrated above is one that most people don't know about, because people just don't hit it often in practice.

1 Like

Choosing between static factory methods and initializers has an impact on other aspects of the API. So I think it comes down to a preferred style and I can see value in having both options and adding expanded to benefit from inits. Also, I didn't know C++ had a generalized way of forwarding arguments, that's nice.

2 Likes

Forgive me, but these restrictions sound a lot like putting the compiler before the syntax. Are they going to be justifiable to an ordinary developer who's trying to use this feature in their library? Or just result in a lot of cursing...

I think this idea as proposed has extreme potential for creating confusion when reading code. Nowhere in my codebase is there a method declared as func linearGradient(materials: [Material], startPoint: Double, endPoint: Double) yet somehow this call compiled!

This gets even worse with more arguments to the target initializer. (To say nothing of multiple @expanded parameters!) In fact, I see the same combinatorial explosion mentioned in the motivation, but now it affects the reader, not the writer. And the reader is generally at more of a disadvantage, even in a codebase they're familiar with. How do I find the source for this function? Somehow I have to know that there's a Gradient type that maybe lines up here, and then reason backwards to realize there must be an @expanded parameter.

At a minimum I think the "expansion" must be marked out at the call site. Off the top of my head we could prefix the "borrowed" labels with $, since we use those already for compiler-generated identifiers. (Though I fear that would be awfully ugly.)

12 Likes

I'm largely talking about the semantics of the feature. Is the semantic model that @expanded is sugar for a call to .init() around the expanded arguments (as currently proposed)? How are expanded arguments bound? Does argument binding use information from the expanded type's initializers? These are all questions that impact the semantics of expanded parameters. Some of the restrictions are also fundamental to language semantics. For example, it's not possible to use @expanded with an existential type because it's not possible to construct a value of this type; you need to explicitly construct an instance of a concrete type.

A language feature does need to be feasible, though, and compile-time performance is a big factor here. I don't consider it feasible to implement argument-to-parameter binding semantics that require type analysis, because that would involve the compiler attempting different combinations of these bindings. This restriction already exists today with default arguments, and it's hardly ever an issue in practice because code that would actually rely on this behavior is pretty uncommon. I suspect the same will be true of this feature. I anticipate a large set of use cases for this feature will be with simple value types with a member-wise initializer that allows the developer to pass in either an existing instance of the type, or pass the members directly, e.g. passing (point: cgPoint) versus (x: x, y: y).

All of that said, it's completely fair to make arguments against this feature because it's obfuscating an initializer call and makes the code, especially overload resolution, harder to reason about. Part of the purpose of this pitch is to gather thoughts about whether this feature is worth adding to the language, gather more use cases, discuss the potential for misuse and confusion the feature could cause, etc.

I understand the arguments about making the code less readable, but this particular point sounds like a tooling problem. I don't see any reason why SourceKit wouldn't be able to provide quick help and jump to definition info for both the enclosing function call and the expanded initializer.

3 Likes

It's also important to consider that adding static factory methods to a lot of API is not scalable. Not to mention that @expanded clearly indicates that what's accepted is a Gradient; the other autocompletion prompts are just for convenience. This will help clients grasp the API more effectively, which ā€”in my opinionā€” is a huge win.

Since this works, perhaps the attribute should be @expandable.


Other feedback: I think @expanded should only be allowed on parameters with _ labels. This would leave the door open for future extensions where the compiler could automatically concatenate the parameter label with the expanded labels:

func drawLine(
  start: @expanded(joinStyle: camelCase) Point,
  length: Double,
  angle: Angle
) 

drawLine(startX: 10, startY: 15, length: 100, angle: .zero)

This also enforces the idea that, even when the parameter is expanded, it is clear from context what the expanded labels refer to.

2 Likes

I love the idea but think the pitch is just a little too far-reaching as-is*.

It would help if we could all understand the motivation for why the linearGradient methods you've mentioned exist.

That's my understanding as well. The static methods are only discoverability aids, emulating the usability of enumerations. (See ShapeStyle).

And if not for that, they'd only be initializers, not static "factory" methods.


Employing more factory methods is the direction the language is going in right now. So I think the solution is to just make that easier.

public extension ShapeStyle where Self == LinearGradient {
  // Creates an alias for all LinearGradient initializer overloads
  @factory static func linearGradient
}

I don't think factory is the best word, but it is a historical term of art.


And I don't know that we need this flexibility, but there are probably use cases for when the returned type is not Self:

public extension ShapeStyle where Self == LinearGradient {
  // Creates an alias for all AnotherType initializer overloads
  @factory(AnotherType) static func anotherType
}

I might be interested in being able to opaque the instances returned from these sorts of methods, e.g.

@factory static func mysteriousInstance -> some PublicProtocol

ā€¦but because of the way we're organizing these things now, there's no utility in that. The public methods can't be available unless the concrete types are public.

// Extension cannot be declared public because its generic requirement uses an internal type
public extension PublicProtocol where Self == InternalType {
  // Cannot declare a public static property in an extension with internal requirements
  @factory public static func mysteriousInstance -> some PublicProtocol
}

You could get around this with the previous parameter syntax, but you'd still have to put this method somewhere, and it can't be in an extension that relies on InternalType. :pensive:

@factory(InternalType) public static func mysteriousInstance -> some PublicProtocol

(* Specifically, I think trying to automate the invisibling of more than one init call is too complex.)

1 Like

Awesome pitch! Would @expanded work in closures?

struct Foo {
  var bar: (@expanded Bar) -> Void
}
struct Bar {
  let baz: Int
}

This would be super useful for supporting named parameter syntax for closures:

foo.bar(baz: 42)
// vs.
foo.bar(.init(baz: 42))

If not as currently pitched, could that be another future direction?

5 Likes

I like this. I wonder if @constructible wouldn't be clearer though, since we already use "construct" as a synonym of initialize.


As said before, there is space in the language for both initializers and static factory methods. With that said, I think what you suggested is interesting, but it weakens the control API authors have. With @expanded we can determine exactly which parameters can be expanded, in which methods that can happen, etc. What you suggested would allow types to opt-in to this forwarding behavior, without a mechanism for specific methods/parameters to opt-out/in. Thus, creating the possibility for misuse, and taking away the control of API authors.

1 Like

I really like the idea of this pitch, although I can clearly see some problems with the limitations of the proposed argument-to-parameter matching. Maybe some of these limitations could be lifted, through the use of a different matching algorithm?

This e.g. could possibly be changed to look for the last label that matches 'third' instead of the first. That would lift the limitation with repeated parameter labels.


I would really like, if generics got at least a little section in future directions.
I can imagine that this feature could work really well with generic types.

Take e.g. RangeReplaceableCollection.append(_:). With expanded parameters we could basically turn this into std::vector::emplace_back(), which would be super cool:

// In stdlib
public protocol RangeReplaceableCollection: Collection
  where SubSequence: RangeReplaceableCollection {
    
    // ...

    mutating func append(_ newElement: @expanded __owned Element)

    // ...

}


struct Point {
    let x: Double
    let y: Double
}

var array = [Point]()

array.append(x: 5, y: 6)
2 Likes

Iā€™m pretty strongly against this feature, for a handful of reasons:

  • As noted, this makes it harder for a person (not an IDE) to go from a call site to a method declaration (perhaps in documentation). Default arguments and trailing closures already do this, but at least those forms only omit labels; they donā€™t add new ones.

  • Whatever the label-matching rules areā€”and I confess I didnā€™t look at them too closely, because it doesnā€™t affect this pointā€”thereā€™s the possibility of an overload of linearGradient shadowing an initializer of Gradient. I suppose this isnā€™t too likely to happen in practice, especially given the reduction in overloads this feature will bring, but default arguments do make it more likely.

  • Not all initializers are equal. Gradient doesnā€™t conform to Codable, but it could, and then code completion would offer .linearGradient(from: <Decoder>, from: <start: CGPoint>, to: <end: CGPoint>). Itā€™s not the double ā€œfromā€ that bothers me; itā€™s that itā€™s now less clear how the arguments will be used to construct the output. Someone reading this code could reasonable ask if there might be coding keys that control the SwiftUI modifier, not just the gradient!

  • Library evolution. Iā€™m a pretty strong believer that public APIs should be explicitly opted into, if not explicitly written, but this allows the API of one type to implicitly affect another. I think itā€™s fair to push back on that objection in general, because itā€™s little different than just constructing the Gradient and passing it to .linearGradient. But cross-module extensions twist that some. If I add an initializer to Gradient in my own module, can I use that for an expanded parameter? If SwiftUI adds an overload of linearGradient that conflicts with my initializer, my code will stop compiling, right? This is a little different to us both extending the same type and having a conflict, because now thereā€™s only a conflict by composition. (You could address this by saying only initializers in modules visible to the module containing the expanded parameter are allowed, but I think that makes the user side of the feature more complicated.)


I donā€™t want to disparage the work youā€™ve done; this is a well-crafted proposal and youā€™ve got an implementation to boot. But ultimately I think this is a feature that makes the language more complicated in exchange for avoiding some extra typing by library developers trying to add conveniences for their users. None of these overloads are essential for the function of the library; they just make it easier to use. And thatā€™s a valuable goal, but I donā€™t think this is a better way to do it than just writing them out.

55 Likes

Thank you (genuinely) for your thoughtful criticism!

An aside (and sorry if this is off-topic): I think this is understood, but in case it's not, I'll reiterate: the exploration/discussion is valuable regardless of whether or not this feature ends up getting accepted. If it turns out this feature isn't right for the language, we still will have established some valuable principles for why that's the case. In either case, there are also valuable learnings surfaced for all of us participating in the discussion.

34 Likes

The idea seemed sound at first, but after some thought I feel it's a lot of compiler/maintenance drawback just to save 7 characters:

method(a: a, b: b, c: c)
vs
method(.init(a: a, b: b, c: c))

7 Likes

Sorry, yes, I should have said "semantics", not "syntax", indeed.

I suppose to some degree this is a philosophical question -- and I know this point has been discussed in the forums before. But I disagree with the idea that a language feature can be brought over the line of usability by a secondary tool. I think it must stand on its own as far as the inspectability of the source. With no disrespect to the folks who work on SourceKit -- it is surely a hard problem -- it simply does not always work, on large projects, and on complex expressions/call sites, like the ones that I anticipate this feature producing. (And outside of Xcode.) Text search is always going to be an important fallback, in my opinion.

5 Likes