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

It seems to me like we could version breaking ABI rules like this in a way that we only take advantage of them if you target newer compiler or runtime versions (and only ever have). That would be another axis of complexity to have to reason about, though.

A more straightforward approach to these sorts of ABI fixes might be to just implement them behind an ABI version flag. That way other platforms that don't have ABI constraints can benefit right away, as can new platforms that might eventually be introduced that can take an ABI break.

2 Likes

The whole bottom row represents a second way to write things already expressible by the top-right cell. We didn't bother to do that for the bottom-right row, so we already have this problem today:

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

extension Foo where T: Hashable, T: Comparable {
  func bar<U>(_: U) {} // could be equivalent to above, but isn't
}

SE-0267 exposes the problem in a lot more places, and while there's an obvious way to canonicalize the newly-allowed declarations to an existing syntax (bottom-left cell -> top-right cell*), we have both the top-right and bottom-right cells in existence today. So the options would be:

  1. Moving a constraint between the extension and the member is always breaking (as proposed).
  2. Moving a constraint between the extension and the member is okay if the member is not itself generic (still ABI compatible but a much more subtle rule).
  3. Moving a constraint between the extension and the member is always okay (ABI-breaking for instances of the bottom-right cell without extra annotations, in order to canonicalize them to the top cell).

* We can't canonicalize to the bottom-right cell because it's not obvious which constraints go on the extension and which go on the function. If it's "all of them that can go on the extension", that's the top-right cell. We could in theory canonicalize to the bottom-left cell, but that'd be extra breaking since it's the one cell that isn't possible today, and it loses some nice properties for partial names.

6 Likes

I suppose, based on our experience with opaque result types, that any new mangling will make this feature not back-deployable by default.

The proposal does not mention a requirement for newer runtime, which means this should still be back-deployable.

Only new type manglings need new runtime support, though the Swift 5.1 has additional fallbacks that can allow for the compiler to avoid needing to emit new manglings when targeting older OSes. Our existing mangling scheme ought to be able to cover naming symbols using this feature.

This is what Slava originally said on this matter:

Though I don't see how the lexical structure breaks when we exchange constraints between the extension and the member.

Does it matter at all to the demangler where the constraints are placed?


Yes, that will be allowed too since you can already constrain the extension instead.

I think you misunderstand, so let me be more clear with a code example.

I want to be able to write this:

extension Sequence {
    func toSet() -> Set<Element> where Element: Hashable {
        return Set(self)
    }
}

Instead of this:

extension Sequence where Element: Hashable {
    func toSet() -> Set<Element> {
        return Set(self)
    }
}

It isn't clear from the proposal whether that is possible. It is clear that you cannot use constraints on protocol requirements. What about symbols declared in extensions?

There is a good reason to disallow this in the context of protocol requirements. It would be confusing to have a requirement with such constraints. What would that even mean in a case where Self or an associated type does not meet the constraints?

On the other hand, the meaning is quite clear in the code I have written above. The only reason I can imagine for disallowing the former is implementation difficulty. Ideally if we're going to lift this limitation we would do it everywhere it is sensible to do so.

I did mention it in a generalized form in the introduction note, but apparently it should be made more eye-catching:

Only declarations that already support a generic parameter list and being constrained via a conditional extension fall under this enhancement.

Unless the constraint is on a protocol requirement (we haven't modelled those yet), there is no misunderstanding :slight_smile:.

P.S There are some tests for protocol extensions too.
https://github.com/apple/swift/pull/23489/files#diff-f058e6035325de0d3eeeb9cf08946e69

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
Terms of Service

Privacy Policy

Cookie Policy