SE-0267 — `where` clauses on contextually generic declarations

Just out of curiosity, why do you want the former rather than the latter?

I think the proposal itself answers this question quite nicely, just in the context of concrete types. I want to be able to group declarations as I desire, and not be forced to put them in separate extensions because of a language limitation.

4 Likes

"Demangling" isn't the only client of mangled names, but in this particular case the concern is about canonicalization. Canonicalization means that given a set of constraints they're always printed in the same way, even if we got them differently.

1 Like

Right, adding code is perfectly fine. Moving constraints between extensions and members is also fine with the rare exception of doing so to public symbols in a library with a stable ABI.

-1/2 as written. I could be convinced that this is a good idea, but for now it makes me uncomfortable for several reasons:

  • Swift tries hard to provide exactly one correct way to do something syntactically. This introduces an ambiguity of which way is best from a style point of view, especially in cases where you have a single method in a constrained extension.
    • When we previously moved the preferred location of function where clauses from the generic parameter definitions to the end of the function declaration, we also introduced a diagnostic pointing people to the new location.
    • At this point we are likely too far down the road of source stability to push people onto this new form.
    • Even if we wanted to definitively say that where clauses should always be included on the function instead of the extension, we couldn’t because properties, etc. would be excluded (at least for now).
    • In my mind this creates an uncomfortable style inconsistency. This additionally means that if swift-format eventually decides to pick one flavor over the other for consistency sake, the choice of which way is best is rather subjective.
  • Even though we already have two sets of where clauses in relevant examples (one on the extension and one on the function), they currently have different semantic meanings, which makes the original “artificial” restriction make sense. Constraints on the extension always reference the type being constrained and the impact is what additional members or conformance that type is given. Constraints on functions always reference parameter types of that function and never impact the member’s availability on a type (until specific argument types are considered).
    • Implementing the premise of this proposal would eliminate this distinction. In my mind, this creates an uncomfortable semantic inconsistency that would make it more difficult to reason about a particular method declaration. In order to determine if a given method will be available on a specific type, I need to manually parse the where clause to see if it references any outer generic parameters.

However, I like the main idea behind this proposal because:

  • Grouping related methods is often desirable. That said, pragma marks are already a reasonable way of grouping related extensions.
  • This change would actually benefit readability greatly in the case of a method declared in the original object declaration that depends only on outer generic parameters. I’m just concerned because it might hurt readability in cases where the where clause of a generic method references both inner and outer generic parameters.

I would be open to a solution that somehow could reconcile the first list with the second.

4 Likes

I can't say that this is my impression at all. Or it is not having a lot of success, if that is indeed a goal. e.g. you're free to choose between func f<T: Equatable>(…) and func f<T>(…) where T: Equatable according to personal taste, amongst many other examples. I personally think it's fine to provide some stylistic choice, and to tailor your style to whatever reads better in context.

5 Likes

One other thought on this: would it be possible to emit members of the bottom-right cell under both the original and canonicalised manglings if the deployment target predates the canonicalisation change? That would trade some code size for an easier long-term deprecation path.

1 Like

That's potentially doable, since it's just functions, initializers, and subscripts we're talking about. @Slava_Pestov, what do you think?

Nested classes, enums, structs and type aliases too, no?

This would say otherwise :wink:

1 Like

I'd say that there are actually very few variants for closure declarations. Technically, there are at least 2 ways to declare an Int

let a = 1, b: Int = 1

But the latter is actually not favored in most situation, and is used only when the type checker fails, or as stylistic choice. If we exclude the implicit-explicit typing variances, we'd only have:

var variable = { (a: Int, b: String) -> Int in ... }
funcName { a, b in ... }
funcName { ... } // Uses $0, $1 instead

In that sense the true variance would be the choice between named arguments and synthesis ones.

I suppose in the sense of "assign it to a variable/property", "type alias", or "as a parameter" that's true, but syntactically there are at 4+ ways to do each of those which makes it feel like a tons of options. But this is a digression anyway so it doesn't really matter :innocent:

The more I use a language, the more I appreciate this type of flexibility. The language has to balance that with making it easy for new users to learn and understand. Artificial constraints like the ones removed by this proposal IMO don't really help new users, so I'm ok with them being removed.

1 Like

In terms of user choices, we actually already have one similar to this proposal: access control:

extension Foo {
  public func bar() { ... }
}

/// Equivalent to 

public extension Foo {
  func bar() { ... }
}

It gets a little murky with fileprivate, but yeah, it's not totally unprecedented.

2 Likes

That’s actually a convincing precedent. I may have overstated the goal of providing a single correct way (I’d be curious to hear an official perspective on this from the core team), but more on topic my primary concern is this:

Variance in the position of access control doesn’t hurt readability. I could see this proposal potentially hurting it when there are a mix of inner and outer generics in the where clause. However, I could also see how this requires only slightly more gymnastics than finding the extension and checking to see if the where clause impacts anything in your use case.

Regarding implementation, will this require new diagnostics for when the where clause on a method conflicts with a where clause on an enclosing extension?

I would argue that semantically decoupling these two cases is a stretch – in the end, extensions simply act as a bulk function for constraints.

We are allowed to add requirements to outer generic parameters on generic declarations, and not obliged to constrain the declaration's own type parameters:

extension Foo where T: Hashable {
  func bar<U>(_: U) where T: Comparable {}
}

No, because we already have relevant diagnostics for when the target declaration is generic, just like bar in the example above. The proposal simply says that bar does not have to be generic to carry a where clause if the context is already generic, since having the constraint cling to the extension is equivalent.


I believe this is not the kind of ambiguity to be taken negatively. There are things a guard cannot do that an if can and vice versa, but the natural existence of numerous cases in which they can be used interchangeably does not question the usefulness of these constructs or whether they deserve a corner in the language.

2 Likes

Aahh, I somehow did not realize this. In that case, I misinterpreted the extent of the proposal and am now a +1.

Since this is already allowed in the context of generic methods, I agree that it should work anywhere a generic context is evident — even when that context is outer.

2 Likes

Oh, right. Types appear inside other mangled names, so "emit under two names" isn't really possible for those. It still might be worth it to do it for functions, initializers, and subscripts though.

I am honestly glad the community has revived the ABI compatibility aspect of this proposal, and also very motivated to make this happen for future platforms and libraries targeting new OS versions: it just isn't right that a purely syntactic preference can break binary compatibility.

@jrose Is there a problem in simply using the canonical or current mangling for the bottom-right cell depending on the deployment target, rather than emitting members under both variants?

Changing a symbol name is a breaking change, whatever the condition is.

What if you target 10.16 (using new mangling) and an app using your framework target 10.15 or older (using old mangling) ?

It will crash at launch because the symbol is no longer present in the new library.

You mean 10.15 or newer for the old mangling?