String's Comparable conformance doesn't seem particularly useful to me in this context -- but "Sarah" < "Betty"is a valid use of String's existing Comparable conformance. "Sarah".clamped(to: "Betty"..<"Hannah") is a natural extension/composition of the existing capabilities.
It may be debatable whether or not Stringshould be Comparable -- but "this API isn't strictly useful on String" probably shouldn't be a reason to avoid adding additional functionality to the Comparable protocol.
Exactly, this should not be the criterion by which an extension method on a protocol should be judged. i.e.
String conforms to Comparable, so you can easily understand the behaviour of clamped, even if you think that behaviour is not particularly useful. For the same reason, we don't regularly refuse to add new methods to Collection even though they may not be particularly useful on Set or Dictionary.
You can compose the operations to define how clamping works, but IMO the number of types which it is useful to clamp are a small fraction of the types which conform to Comparable. It is an especially important protocol and we must be careful if we add APIs to the types which conform to it.
It isn't just String, it's also Array<String>, Data, URL, etc. I don't think it really is debatable that those types should be Comparable, but I think .clamped would be an unwelcome addition for most of them. That favours an alternate spelling IMO.
I'm not sure āsmall fractionā is fair given that there are a lot of numeric types, just to start with. And the potential harm here is very limited. You haven't made it clear if by āalternate spellingā you mean a free function with arguments constrained to Comparable (equally useless/useful, just perhaps less discoverable depending on your current IDE), a new marker protocol derived from Comparable that only a subset of types conform to, or just defining clamp directly on a subset of types/protocols. I personally don't like any of those alternatives.
Yeah I think that if possible we should avoid minmax style free functions but adding clamped(to:) to all types that conform to Comparable seems a bit extreme.
Yes, limiting clamped(to:) to only being able to be used on types conforming to Comparable makes sense but... IDK maybe its ok but it seems like there is a better option somewhere.
Considering the continuing discussion about Comparable types for which clamp is less sensible, I think putting the primary API on the range types is better, because it seems to me that ranges are more related to clamping than the comparable values directly.
However, as a bit of bike-shedding, I think the verb form (b...c).clamp(d) may be preferable to the gerund form (b...c).clamping(d)
Hi folks, would love to see this feature but agreed on the drawbacks of the aforementioned designs. Here's one option that I don't believe has been mentioned, create a new protocol specifically for "clampable" types that inherits from Comparable. The name needs work but here's an example:
From there we can extend existing numerical types to opt-in to this functionality without affecting strings or other comparable values where it doesn't make sense.
Adding a new protocol can make backwards-deployment tricky, but even then Swift doesnāt usually have single-operation protocols if thereās some underlying structure. So what does make Int and Float and Bignum different from String and Array and Data? The main thing I can think of is ālengthā, but Iām not convinced thatās the fundamental thing weāre looking for.
Other languages donāt seem to separate clamped from Comparable.
Thanks! Thinking more about it, since clamping is really a simple composition of min and max it seems natural to allow clamping on any type that is allowed by min and max, so Comparable.
From a previous example:
"Sarah".clamped(to: "Betty"..<"Hannah")
Agreed this doesn't seem to be useful, at least not any more useful than:
max("Betty", "Sarah")
but we do allow usage of max on any Comparable type today so perhaps we're ok with applying the same thinking to clamped.
Array and Foundation.Data - not actually Comparable!
Character - maybe reasonable to clamp? within certain ranges?
DispatchTime, DispatchWallTime - these do make sense to clamp, and can't comform to Strideable because their stride, DispatchTimeInterval, doesn't conform to Numeric. Maybe it could, though?
Foundation.Date - does make sense to clamp (it's a timestamp), could be Strideable with a stride of Foundation.TimeInterval
Foundation.DateInterval - this is a range, which definitely doesn't make sense to clamp, at least not using clamped, but can still be reasonably ordered
Foundation.Decimal, Foundation.Measurement - can be clamped, should be Strideable
Subscribers.Demand - does make sense to clamp, could be Strideable with Self as the stride
SwiftUI.DynamicTypeSize - does make sense to clamp, definitely isn't Strideable
TaskPriority - does make sense to clamp, arguably isn't Strideable
All sorts of Collection indexes - clamping isn't sufficient to validate them, so I guess not
So Strideable is close, but doesn't cover all the use cases (particularly ordered enums), and we'd have to decide if that's okay. It's still useful to observe that Strideable is sufficient, i.e. there are no Strideable types that should not be clampable.
My reservations are due to there being lots of types which conform to Comparable so they can be sorted, but which don't necessarily want a .clamped function as part of their API. If there was a separate proposal for String.clamped, would it be accepted? I think not. So why accept this member because it entered String's API through another path?
It isn't that Comparable is the wrong constraint (I think it's the correct constraint), or that the operation is ambiguous. There is an argument that a Collection may want to clamp its contents (like numpy's .clip), and that this more useful operation could be confused with .clamped if you can't follow the types of the arguments. I think that argument is a bit weak but not not entirely meritless.
Still, protocols add members to their conforming types. That means that when we add members to a protocol, we need to consider carefully what the impact will be on all conforming types, and this is especially true of basic protocols like Comparable, Equatable, Hashable, etc.
My preference is for a clamp free function. I don't think it will limit discoverability - after all, people don't seem to have a problem with existing free functions like min, max, zip, etc., I don't think it is any less obvious than the member function, and it avoids having every Comparable type inheriting a possibly-unwelcome new member. So I see one benefit and no harm by making this a free function.
Wow, this seems like an oversight. We even have an accepted proposal for tuples to conform to Comparable.
I think itās correct for Data to not be Comparable; I canāt think of a need to order or sort it, ever. Array could probably be lexicographic like tuples, though.
The free functions min and max are mathematical functions that most people learn with free syntax in school. I donāt think clamp is the same.
@jrose So I guess it makes sense for clamped(to:) to have it's home inside of Strideable if its better to undershoot rather than overshoot by placing the method in Comparable where we will get some types where having a Clamped(to:) is not very useful.
Maybe if we go with Strideable we should also do an audit of all types that don't currently conform to Strideable but maybe should.
String.clamped would probably not be accepted, simply because there's not a sufficiently compelling use case to add it specifically to String. But it's not harmful for it to be there. .clamped() can be implemented just with Comparable-ness, so I don't see a reason why we should arbitrarily restrict it to Strideable.
More importantly, in a generic context, I think Comparable is the right protocol. It would feel natural for a SemanticVersionNumber to be Comparable, for example, and it would also make sense for it to have a .clamped() function. But it doesn't make sense for it to be Strideable, and it seems silly to add a separate protocol just to exclude this function from appearing on String even though it would work just fine on strings.
I think putting it on Comparable is fine. Not ideal but fine ā and not worth making a separate protocol just for it. The fact that max and min work on String kinda seals the deal, at least in my view.
So having a bunch of non-compelling APIs on String (and other Comparable types) is not harmful?
I think the point has been made that clamped doesn't make sense for every Comparable type, but the argument seems to be: "we don't care".
It's not an unreasonable perspective (really, it isn't), but I'd like to be explicit about it - we're saying that we don't mind if a bunch of types gain an API that people will never use, because the member function is so much better than the alternative spelling? If that's where we as a community think the best balance lies, then so be it, but IMO we should be thinking about it in terms of that balance.
I agree. The very next sentence after the one you quoted says as much.
Yes, I think explicitly that the member spelling is much better than the free function spelling, and strongly prefer it. I think the downside of having additional API also available on String is minimal, so that tradeoff seems fine to me.
This echoes my thoughts exactly. The ability to use clamped in a chain of methods, possibly together with ?, is one of the reasons it is so useful. The free function is both less discoverable and less readable.