SE-0358: Primary Associated Types in the Standard Library

The angle-brackets-after-a-protocol-name feature can be used to constrain associated types to protocols, not just to constrain them to one specific type. I'm expecting that beyond cases like Collection<String>, protocol constraints will in fact be the more typical use case.

The term "same-type requirement" comes from the title and text of SE-0346, "Lightweight same-type requirements for primary associated types".)

If this choice of terminology invites misunderstanding, perhaps it would make sense to update both of these proposals with a better phrase.

Hm, this is an extremely good point; I think this is an oversight that needs to be fixed. But rather than removing the primary declaration, I would prefer to correct the proposal to use the right type.

(I believe that OptionSet.Element is only customizable because of legacy compiler limitations that are no longer relevant. If we designed this protocol today, we'd define it as protocol OptionSet: RawRepresentable, SetAlgebra where Element == Self. (Possibly also adding RawValue: FixedWidthInteger.) However, such constraints weren't possible / did not work correctly at the time this landed. And of course, fixing this now would be both ABI- and source-breaking.)

Judging from use cases (i.e., guideline 1), RawValue is clearly the right choice for the primary type: for example, most of OptionSet's utility comes from extension methods / default implementations where RawValue: FixedWidthInteger.

Question is: would OptionSet<RawValue> run afoul of guideline 2 (clarity of point of use)? To me, it feels fairly obvious (in hindsight) that some OptionSet<UInt32> would mean RawValue == UInt32 -- especially given the note on Element above.

This case is somewhat similar to LazySequenceProtocol, where naively one might expect Element to be the primary type (e.g., for consistency with Sequence), but once one thinks about it a little more, Elements becomes the "obviously" correct choice. In that case, I didn't care enough about the matter to press it -- I expect most people who need to be explicitly mentioning the lazy protocols will be comfortable with using the classic generic constraint syntax. (I did conveniently ignore @Ben_Cohen's opaque return type case, where some LazyCollectionProtocol<some BidirectionalCollection<String>> would enable additional functionality not currently achievable with where alone.) To me, OptionSet's case feels far less subtle than the lazy protocols, so I want to press on this a bit harder.

Guideline 3 doesn't seem overly relevant here -- clearly there would be some utility to being able to spell OptionSet<some FixedWidthInteger> or even OptionSet<Int64>. OptionSet doesn't feel like it is in the same category as ExpressibleByIntegerLiteral or KeyedEncodingContainerProtocol; although the use cases aren't wide spread, they clearly exist.

<off topic>

I would love for that to be the case, but sadly this is not spelled out as a requirement in OptionSet's documentation. It is true for the default implementations provided, at least as long as the RawValue is one of the standard fixed-width integer types. Manual conformances to OptionSet that use different implementations ought to be rare (this is largely a utility protocol), but I'd be willing to bet that someone out there has found a use case where doing that makes sense. :nerd_face:

</off topic>

6 Likes

Indeed, this is exactly my position. The stated goal for SE-0346 was to "make generic programming in Swift feel more natural and approachable". Generally, I don't think the Standard Library or the API guidelines should second guess this goal by preventing Swift developers from using the lightweight constraint syntax -- unless of course there is a clear reason to do so.

My intention with the third guideline was to cover cases like ExpressibleByIntegerLiteral or KeyedEncodingContainerProtocol. These are protocols that have one associated type, and it would technically be a suitable primary, but actually declaring it as such would be pointless, as these protocols aren't really designed to be used generically in practice. The guideline is also there to reassure folks that it's okay not to declare a primary type if doing so would go against one of the other points.

(I expect that guideline 2 (clarity at the point of use) is going to be a far bigger sticking point than guideline 3. Personally, I find it regrettable that we don't support mandatory (or even optional) argument labels within angle brackets, limiting Swift's eminent readability in this area of the language. Perhaps this would be an interesting direction for future work.)

I'm misusing OptionSet for a larger bit set with non-default Element and RawValue types. I should probably use SetAlgebra instead.

Off topic
/// A type that presents a mathematical set interface to a 256-bit SIMD vector.
///
/// - Complexity: O(*n*) linear time for initialization from a sequence.
/// - Complexity: O(*n*) linear time for coercion of an array literal.
/// - Complexity: O(1) constant time for coercion of an integer literal.
/// - Complexity: O(1) constant time for other `SetAlgebra` operations.
@frozen
public struct SetOfUInt8: OptionSet, Sendable {

  public typealias Element = UInt8

  public typealias RawValue = SIMD4<UInt64>

  public var rawValue: RawValue

  @inlinable
  public init(rawValue: RawValue) {
    self.rawValue = rawValue
  }
}

I'm not sure if either OptionSet<Element> or OptionSet<RawValue> should be proposed. The protocol seems to exist mostly for the default implementations (and the non-failable initializer).

1 Like

The Core Team took some time to talk today about the application of primary associated types to OptionSet and Strideable. Decisions about when to make associated types primary are not always going to be clear. However, we feel that the guidelines in this proposal should capture the considerations that should go into those decisions, to the point of dictating those decisions whenever possible. If the proposal authors feel that Strideable.Stride should be a primary associated type, that's fine, but the guidelines need to cover the reasons why that's the right decision; and similarly with OptionSet.Element and so on.

In that spirit, we would like to invite a deeper discussion about the guidelines. These controversial cases seem like excellent opportunities to discuss why the guidelines should or should not indicate that an associated type should be primary. Community members might also wish to suggest protocols outside the standard library that we can consider how to apply the guidelines to.

If this discussion takes longer than the current review period, that's fine; the Core Team can ensure that non-controversial cases are officially decided so that we're not held back by the 5.7 schedule.

14 Likes

For me, the completely non-controversial primary associated types are captured by a much narrower subset of the first guideline ("Let usage inform your design"), though somewhat wider than just "elements of collections." I know that @lorentey understands the guideline to mean looking at extensions of the protocol and other usages, but the specific usage that I have in mind is this:

Do concrete types that implement the protocol have generic parameters which correspond to this associated type? (For instance, Array<Element>, Set<Element>, etc., are all conforming implementations of Collection<Element>, and SIMD4<Scalar> conforms to SIMD<Scalar>.)

I surmise that this feels natural and uncontroversial (at least, to me) because the syntax used here deliberately parallels that of concrete types, and if FooImpl<T> and BarImpl<T> meet the bar of being clear (guideline 2) and useful (guideline 3), it seems natural that FooAndBarProtocol<T> will as well.

Of the currently proposed protocols to have primary associated types that are non-controversial and don't fall into that narrower sort of "usage," I can see only four: Identifiable, RawRepresentable, Clock, and InstantProtocol.

The latter two were added recently, and the underlying protocols and their implementing types are very new, so I can't really speak to them from experience or intuition, although they too may benefit from reconsideration along these lines if the overarching notion is generally accepted.

The former two are unified by something more than having a natural preposition to describe the relationship between associated type and protocol ("identifiable by" and "representable by"; guideline 2), but rather they additionally convey some notion that the primary associated type is in some way a stand-in ("identifiable," "representable"). This distinguishes them from the Strideable case where the Stride is not a stand-in for the type that it's associated with.

To put it another way, perhaps it is important that the relationship—association, one might say—between a primary associated type and the protocol has to not just be clear [edit: and not merely empirically the most often constrained type per @lorentey’s interpretation of the first guideline] but, well, meet some bar of semantic primacy.

9 Likes

The Core Team talked today about what to do with this proposal. Reviewing the feedback, the application of primary associated types to most protocols in the standard library seems to be largely uncontroversial, but there's significant discussion about what the guidelines ought to be in general and (in particular) how they ought to apply to Strideable and OptionSet. The Core Team has decided to accept those uncontroversial portions of SE-0358. The remainder of the proposal remains in review, and the review has been extended for three weeks, until June 20th, 2022, to discuss the general guidelines and figure out how they ought to apply to those two protocols. I see no reason not to continue the review in this thread.

16 Likes

Let usage inform your design.

I can't find any such OptionSet usage in the standard library. Are there useful algorithms, without knowing the custom static var properties that give meaning to each option set?

I can imagine a useful generic AnyOptions<Element> or AnyOptions<RawValue> structure, where both associated types are the same fixed-width integer. It would be intended for options imported from #define macros, when SetAlgebra methods are preferred over bitwise operators.

For example:
#define SQLITE_OPEN_READONLY  0x00000001
#define SQLITE_OPEN_READWRITE 0x00000002
#define SQLITE_OPEN_CREATE    0x00000004
public var SQLITE_OPEN_READONLY:  CInt { get }
public var SQLITE_OPEN_READWRITE: CInt { get }
public var SQLITE_OPEN_CREATE:    CInt { get }
public final class Database {

  public init(options: AnyOptions<CInt> = []) throws {
    var options = options
    // If necessary, add the default "mode=rwc" flags.
    if options.isDisjoint(with: [SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE]) {
      options.formUnion([SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE])
    }
    /*...*/
  }
}

(Highlights by me)

This is a valid analogue, but I think it's too narrowly applicable to be generally helpful. As I see it, there are two problems with it:

  1. First, the role/meaning of T in FooImpl<T> isn't at all clear to me -- this type name alone includes no indication whether T stands for something like the Element type of an Array, the Base type of a Slice, the Root of a PartialKeyPath, or something completely different. Nor is this something that can be reasonably clarified through API design, as Swift provides no tools for API designers to clarify the role of such type arguments at the point of use. (Unlike with function invocations, where we can use mandatory argument labels to help clarify their meaning.)

    So in my view, FooImpl<T> and BarImpl<T> do not meet any reasonable level of clarity at the point of use, and so therefore the premise of the implication quoted above is false -- the statement does not tell me anything useful about the clarity of FooAndBarProtocol<T>.

  2. Second, this analogue does not seem to be applicable outside the Collection protocol hierarchy and its immediate neighborhood (such as AsyncSequence). General-purpose Collection types are indeed usually generic over their Element (or their base collection type, in the case of generic collection transformations, such as those conforming to LazyCollectionProtocol). However, this isn't true for specialized collections (such as String or Bitset), and it seems not to be true in general for protocols that aren't modeling containers/streams.

    Identifiable, RawRepresentable, Clock, InstantProtocol do not seem rare exceptions to me -- rather, these feel like rather typical protocols outside the collection hierarchy. In my view, Strideable and OptionSet are firmly in this category as well.

    (Of course, we could choose to limit the use of this new language feature to Collection-like protocols. However, that seems overly restrictive to me.)

In the specific case of Strideable, I don't see any issues about clarity at point of use whatsoever. On a superficial level, it would not be entirely unreasonable to say that Strideable.Stride feels too similar to Numeric.Magnitude for comfort. However, to me there is a very clear difference between these two protocols: the Strideable protocol consists entirely of operations taking or returning a Stride value, while Magnitude plays a far less crucial role in Numeric. Indeed, Strideable is every way as much a single-purpose protocol as Identifiable or RawRepresentable is -- all three of these protocols are built around their sole associated type, and even share the root of their name with it.

Identifiable types aren't usually generic over their ID; RawRepresentable types aren't typically generic over their RawValue, and Strideable types are rarely if ever generic over their Stride. I don't see how this indicates anything about whether or not we should allow using the lightweight constraint syntax with these protocols.

I believe I have already addressed the usefulness of constraining Stride.

In general, as long as there is a clear choice for a primary associated type, I think it's preferable to err on the side of defining one, rather than omitting it, even if the use case seems marginal. (Which I really don't think it is in Strideable's case.)

As we've seen with Clock, just because I can't think of an obvious use case for enabling the use of the lightweight syntax, it doesn't follow that nobody can. To me, the case for some Clock<Swift.Duration> only seemed obvious in hindsight. (Thanks @stephencellis for pointing it out!)

For what it's worth, the Standard Library does contain a number of useful algorithms on OptionSet that use a where RawValue: FixedWidthInteger clause; they supply default implementations to some core SetAlgebra requirements.

I don't see a reason to actively prevent this code from using the OptionSet<some FixedWidthInteger> syntax (whenever it becomes available for use in extensions).

As for usages outside of the stdlib, I can certainly imagine someone defining new algorithms, such as a method for getting the complement of an option set:

extension OptionSet<Int> {
  func complemented() -> Self {
    Self(rawValue: -1 ^ self.rawValue)
  }
}

(This may or may not be a wise thing to use, but I believe it does work.)

Or I can imagine that someone working on a new serialization facility might want to define generic methods that can work with any option set of a certain raw value:

extension MySerializer {
  func write(_ value: Int)
  func write(_ value: some OptionSet<Int>)
}

Granted, these examples have a distinct smell of desperation about them -- OptionSet is far more of a utility protocol than most of the other standard protocols.

For what it's worth, a quick code search on GitHub does readily uncover such RawValue constraints being used in actual real-life code.

Do we have a good reason to actively prevent people from using the new syntax for these cases? E.g., do you expect to repeatedly misunderstand OptionSet<Int> to mean a constraint on the Element type?


Hm; that is a remarkably high expectation for any set of API guidelines. Removing all subjective decisions seems like an unrealistic expectation, but perhaps we can tie things down a little more.

The "usefulness" guideline appears to be the weakest in this regard:

  1. Not every protocol needs primary associated types. Don't feel obligated to add a primary associated type just because it is possible to do so. If you don't expect people will want to put same-type constraints on a type, there is little reason to mark it as a primary. Similarly, if there are multiple possible choices that seem equally useful, it might be best not to select one. (See point 2 above.) For example, ExpressibleByIntegerLiteral is not expected to be mentioned in generic function declarations, so there is no reason to mark its sole associated type ( IntegerLiteral ) as the primary.

I intended this merely as a way to carve out exceptions for cases like KeyedEncodingContainerProtocol, ExpressibleByIntegerLiteral, where there would be a clear, unambiguous choice for a primary associated type, but it seems pointless to define one, as these protocols are highly unlikely to be ever used in generic context.

Rather, this guideline has triggered arguments on whether it it'd be useful enough to allow lightweight constraint syntax on protocols where there are clearly some use cases for constraining the candidate primary type.

What if we reworded this requirement to something that provides less room for subjectivity?

  1. Err on the side of defining a primary associated type. As long as there is a clear, unambiguously right choice for a primary associated type, and there is even a marginal use case for defining one, it's better to enable the lightweight constraint syntax rather than arbitrarily prohibit its use.

    Conversely, do not feel obligated to select a primary if there are multiple possible choices that seem equally useful, or if the choice would lead to persistent confusion, or if the protocol isn't designed to be ever used in generic context. For example, ExpressibleByIntegerLiteral is not expected to be mentioned in generic function declarations, so there is no reason to mark its sole associated type (IntegerLiteral) as the primary, no matter how obvious a choice it would be.

That said, I do not wish to cut off discussions about requiring a higher level of usefulness, if fellow members of these fora feel it'd be worth exploring this a little more.

@xwu, @Karl, @benrimmington: You have recently raised objections of this nature -- do you feel it would be actively harmful to allow the lightweight syntax on unambiguous-but-perhaps-marginal cases? (Why?)

In other words, would you be willing to accept a certain number of infrequently used declarations in the interest of making the guidelines less subjective? If not, do you have a suggestion for a guideline replacing point 3 above that sets a higher bar for usefulness, without overly increasing subjectivity?

It would be extremely helpful if readers of this forum would try applying the proposed guidelines (with or without the clarification above) to protocols defined in their own code. Can you find examples where the guidelines do not apply well or where they lead to undesirable results?

6 Likes

I agree that it’s not realistic to fully achieve that. But yeah, it does feel like the current guidelines don’t capture all of the kinds of reasoning going into the protocols in the standard library.

1 Like

Bringing this review back to the top now that WWDC is over.

2 Likes

I tried to browse the search results. The vast majority were copies (not forks) of the standard library. Several others were for iterating an option set, all based on the same answer from Stack Overflow. I didn't find any interesting code, but that isn't an argument for or against OptionSet<RawValue>.

Codable.swift has a default implementation for option sets and enums. Your serialization example could use either some RawRepresentable<Int>; or a protocol composition of some OptionSet & RawRepresentable<Int> is also supported by SE-0346.


Option sets consume/produce either Element or Self. They're expressible by an array literal of Elements. Their RawValue is usually an implementation detail.

I can't think of a suitable preposition (from the second guideline) to describe OptionSet<RawValue>.


Have the guidelines been tested against the SDK? The following are defined in Foundation, but I'm unfamiliar with their usage.

Protocol Associated Types
SortComparator Compared
ReferenceConvertible ReferenceType
DecodableWithConfiguration DecodingConfiguration
DecodingConfigurationProviding DecodingConfiguration
EncodableWithConfiguration EncodingConfiguration
EncodingConfigurationProviding EncodingConfiguration
AttributeScope DecodingConfiguration, EncodingConfiguration
FormatStyle FormatInput, FormatOutput
ParseableFormatStyle FormatInput, FormatOutput, Strategy
ParseStrategy ParseInput, ParseOutput
DataProtocol Regions, Element == UInt8, Index, Iterator, SubSequence, Indices
MutableDataProtocol Regions, Element == UInt8, Index, Iterator, SubSequence, Indices

To clarify, FooImpl<T> is a stand-in here for those uses of generics which are clear (that is, Array<Element>, etc.)—to reject the premise would be to reject that there can be any uses of generic parameters which are clear at the point of use, which I don't think is what you mean. I think Array<Element> is plenty clear, for example.

Hence, why I proposed a formulation of a rule that extends beyond Collection-like protocols, although it seems your point (2) is that it doesn't extend too far. That would be a fair critique.

One point of concern here is that Strideable types sometimes but not always are their own Stride, and having worked with (on, rather) this protocol quite extensively I can say that keeping the distinction straight is so important for the correctness of generic code.

I really, really want to emphasize again how much I disagree with this proposed metric of usefulness. I really think it is a serious mistake to refer to the number of extensions in the defining library as the metric for providing some feature for end users.

One of the key reasons we add new APIs to the standard library (recalling the core team's guidance on when something meets the bar for inclusion) is that something commonly used is not trivially composed from existing APIs but rather difficult to implement correctly (lots of corner cases, etc.). As the author of a chunk of those extensions you list that constrain Strideable, I can say that they are safely in the "Don't try this at home, kids!" category of tricky, and some of the constraints I wrote (extension Strideable where Stride: FloatingPoint and extension Strideable where Self: FloatingPoint, Self == Stride—refer to my point above about distinguishing the striding and strided types) are directly because of the trickiness of implementation.

Referring to the number of times we actively choose to put work into the standard library dealing with corner cases so that users don't as a reason to provide users with an easier way to do the same thing is not the correct metric: I feel very strongly about this.

Because the syntax can be added in a future version in an ABI-compatible way but not removed if it's added now, and because I do have concerns about active harm (see above—namely, encouraging folks to parameterize protocols to write overly generic algorithms that are tricky to get right due to underspecified semantics, on the basis of where we are using constraints in the standard library for implementation detail purposes precisely to steer users away from that), my humble opinion is that we ought to be erring strongly on the other side and omitting adoption until we have a better sense of usefulness.

8 Likes

I feel similarly.

In particular, this language (though I realize it's not currently part of the proposal text):

would to me suggest that basically every protocol with a single associated type ought to make it primary. The noted exception of ExpressibleByIntegerLiteral doesn't to me provide very useful guidance, given that it exists basically to support compiler 'magic'. The question we repeatedly ask on this forum whenever new protocols are proposed or discussed is 'what useful generic algorithms would this enable?', so excluding protocols which aren't designed to be used in a generic context would seem to exclude almost nothing.

Also, it seems a bit odd to simultaneously allow multiple primary associated types at the language level, while discouraging ever using them in the design guidelines. If we think that their use is so rarely useful and likely to be confusing, should we potentially reconsider allowing multiple primary associated types at all, at least for Swift 5.7?

This just makes life difficult for someone who does have a use case for them, for a type that isn’t of the same nature as those in the standard library.

2 Likes

We should, though, be able to articulate the circumstances in which we would recommend such use. If you are that someone, would you be able to share what use cases you have in mind and what the guidance should then be?

I think it would be a yellow flag, at least, that we have made available a feature and then immediately recommended against its use. There ought to be something (even if niche) that can demonstrate how the feature carries its own weight.

3 Likes

Swift currently has many issues stemming from the legacy tuple splat. Once Swift gets variadic generics, the language could explicitly model (Int, Int) and (a: Int, b: Int) as distinct types that both conform to protocol Tuple<T...> where T... == (Int, Int), or in other words, Tuple<Int, Int>.

This would also apply to functions, which could be modeled as conforming to protocol Function<Args..., Return>. Significantly, this gives us a way to spell the type of generic closures: type(of: { _ arg: some BinaryInteger in arg.bitWidth }) == some Function<BinaryInteger, Int>.


Perhaps less esoterically, a protocol PairwiseIterator<Left, Right> might be handy for zip-like algorithms.

My question was rather geared towards examples of non-hypothetical protocols in use today that would benefit from support for multiple primary associated types in Swift 5.7, from which we can study guidelines that aren’t just “don’t do it.”

Wouldn’t the element type have to be (Left, Right), and thus give good reason for the use of a single primary associated type—i.e., PairwiseIterator<(Left, Right)>? This is why concrete examples are key here.

1 Like

This is what I'd want to see as well. The proposal gestures towards this:

Of course, if the majority of clients actually do want to constrain both Key and Value , then having them both marked primary can be an appropriate choice.

but this feels a bit under-specified to me, especially when it appears under the pretty unequivocal heading Limit yourself to just one primary associated type.

This (and the tuple example) suggests to me a potential guideline along the lines of:

For a protocol with two (or more) associated types that are semantically symmetric (i.e., they could be swapped and the protocol would behave basically the same) it may be appropriate to make both primary.

but that feels fairly narrow and doesn't encompass the hypothetical Function protocol. Good real world examples would be quite valuable here, I think.

1 Like

Just because the Element has to be a tuple doesn’t mean that the tuple should be the protocol’s single associated type. Why force programmers to type the extra parentheses? In fact, without additional constraints imposed by the protocol requirements, a protocol can’t actually enforce that its single associated type be a 2-tuple. That makes two good reasons for using two associated times, IMO.

1 Like

(Edit: I added some extra detail at the top.)

The number of constraining cases in the defining library is, from my experience, a good indication of similar use outside of it. For the specific case of Strideable, a quick search of public repositories on GitHub readily uncovers a number of cases that would benefit from the more readable constraint syntax:

// Current code:
    public init<V>(
      value: Binding<V>, 
      step: V.Stride = 1, 
      onEditingChanged: @escaping (Bool) -> Void = { _ in }, 
      @ViewBuilder label: () -> Label
    ) where V: Strideable {...}
// Proposed option:
    public init(
      value: Binding<Stride<Int>>, 
      step: Int = 1, 
      onEditingChanged: @escaping (Bool) -> Void = { _ in }, 
      @ViewBuilder label: () -> Label
    ) {...}
// Current code:
extension BidirectionalCollection 
where Index: Strideable, Iterator.Element: Comparable, Index.Stride == Int {...}
// Proposed option:
extension BidirectionalCollection 
where Index: Strideable<Int>, Iterator.Element: Comparable {...}
// Current code:
public func +--><Bound:Strideable>(lhs: Bound, rhs: Bound) -> Range<Bound> 
where Bound.Stride == Bound {
// Proposed option:
public func +--><Bound: Strideable<Bound>>(lhs: Bound, rhs: Bound) -> Range<Bound> {
// Current code:
extension TotallyOrderedSet where Element:Strideable, Element.Stride:SignedInteger {
// Proposed option: (assuming future work in this area)
extension TotallyOrderedSet<Strideable<SignedInteger>> {
// Current code:
public struct Real<T> : RealNumber where T:Strideable, T.Stride == T {
// Proposed option: (assuming future work in this area)
public struct Real<T: Strideable<T>> : RealNumber {

How many more of these examples do we need?

The new lightweight constraint syntax is, as explicitly stated, intended to (1)
make "generic programming in Swift feel more natural and approachable" by providing a less scary alternative to classic where clauses, and (2) to enable new features that weren't previously possible (namely, constraining opaque result types (SE-0346) and existential types (SE-0353)).

These goals are dependent on protocol authors adding the necessary primary associated types, starting with the Standard Library. I don't think it would be right for the API guidelines to second guess these goals by artificially restricting the new lightweight constraint syntax to a small subset of standard protocols or by discouraging their general use.

I do believe that for many people, T: FloatingPoint & Strideable<T> is going to be easier to read/understand than T: FloatingPoint & Strideable where T.Stride == T. To me, this alone is reason enough to support this syntactic variant.

I feel that your objection here is, at its core, a critique of Strideable itself, rather than the idea of giving it a primary annotation. I do agree with this assessment. I consider Strideable to have largely been an API design miss -- exactly because it's so impractical to define extensions on it without running into (mostly unresolvable) problems with e.g. handling overflow situations.

In addition, I do not think it would be overly difficult to deal with Strideable not providing a primary associated type. This is a protocol of marginal use. Will being able to type Stridable<Int> rather than <S: Strideable> where S.Stride == Int really make a difference either way? I sincerely doubt it.

However, here is my problem: how can we reasonably define what constitutes a useful enough primary associated type without inviting these sorts of subjective discussions every single time we consider a new protocol?

If we started fixing Strideable's flaws, then at exactly what point would it become an extensible enough protocol to gain a primary associated type? Would allowing a non-trapping overflow path on distance/advanced(by:) be enough? Do we need to expose a cleaned-up version of _step, too? Or is the concept of strideability somehow inherently incompatible with the shorthand constraint syntax?

What happens when someone complains that their preferred shorthand syntax doesn't work for Strideable, and supplies a good use case? Do we fire up a new Swift Evolution proposal every time that happens? Wouldn't it be more productive to shortcut subjective discussions about usefulness by encouraging protocols to gain primary types as long as the choice of which associated type to mark as primary is obvious?

I honestly don't see how we can draw a clearly defined API design line between Identifiable<ID>, RawRepresentable<RawValue> on one side and Strideable<Stride> on the other. The associated types in all three of these protocols are absolutely core to their existence, and so far I did not come across an objective reason not to give Strideable its obvious primary.

The heuristic you proposed earlier was to look at the type arguments of generic types that conform to the protocol. This does not help distinguish between these three cases.

All the shorthand notation does is that it provides an alternative way to spell generic constraints. It doesn't make it easier to implement such extensions, but it does make them superficially easier to read -- as long as the role of the type name within angle brackets is clear, which (I believe) is obviously true for Strideable<some FloatingPoint>. Where exactly is the harm in that?

Should we just err on the other side and restrict primary associated types to Element types on container/stream protocols? That would hamstring this language feature, but it would certainly make the API guidelines easier.

6 Likes