This is part of a series of posts on how Swift resolves ambiguity in overloaded functions. You can find Part 1 here. In the previous article, we looked at why you get a Range
from the ...
by default. The answer? Because Range
has some extra validation that relies on the ...
input being comparable, and this makes the Range
version more specific.
Defaulting to Neither
So, suppose you didn’t want Range
to be the default, but still wanted to benefit from this extra validation?
If your goal was to keep the ambiguity, and force the user to be explicit about whether they wanted a Range
or a ClosedInterval
, you could declare the following:
func ... <T: Comparable where T: ForwardIndexType> (start: T, end: T) -> ClosedInterval<T> { return ClosedInterval(start, end) }
Now, unless you specify explicitly what type you want, you’ll get an ambiguous usage error when you use the ...
operator.
(It might make you a bit nervous to see ForwardIndexType
in the declaration of a function for creating intervals, given intervals have nothing to do wth indices. But don’t worry, it’s really only there to carve out an identical domain to the equivalent Range
function to force the ambiguity for the same set of possible inputs.)
Defaulting to ClosedInterval
What if you actually wanted ClosedInterval
to be the default? This is a little trickier.
If you just wanted to cover the most common case, integers, then you could do it like so:
func ...<T: IntegerType>(start: T, end: T) -> ClosedInterval<T> { return ClosedInterval(start, end) } // x will now be a ClosedInterval let x = 1...5
This works because IntegerType
conforms to RandomAccessIndex
(which eventually comforms to ForwardIndexType
).
Now, this would be a very specific carve-out for integers. You could leave it there, because integers are a special case, being as how they’re so fundamental and it’s a bit odd that they’re defined as an index type too.
But If you wanted an interval for all other type that are both comparable and an index, you’d need to handle each one on a case-by-case basis. For example, string indices are comparable:
// note, this doesn’t even have to be generic, since Strings aren’t generic func ...(start: String.Index, end: String.Index) -> ClosedInterval<String.Index> { return ClosedInterval(start, end) } let s = "hello" // y will now be a ClosedInterval let y = s.startIndex...s.endIndex
Two problems become apparent with this.
First, this gets real old real quickly. Every time you create a new index type you have implement a whole new ...
function for it.
Second, even then it’s still out of your control. You want other people to be able to create new index and comparable types and use them with ranges and intervals, and they aren’t necessarily going to do this. So instead of a nice predictable “you get a range by default”, we now have “you’ll probably get an interval, so long as the type has the appropriate ...
overload”. That doesnt’t sound good at all.
Your best bet at this point would be to force anyone who wants to their type to be useable with an interval to tag it with a particular protocol. This is what stride
does – types have to conform to the Strideable
protocol. So let’s try defining an equivalent for ClosedInterval
. We’ll call it, uhm, Intervalable
.
protocol Intervalable: Equatable { } extension Int: Intervalable { } extension String.Index: Intervalable { } // The following definintions of ... would need // to REPLACE the current definitions. So you // can only do this if you're the author of // ClosedInterval func ...<T: Intervalable> (start: T, end: T) -> ClosedInterval<T> { return ClosedInterval(start, end) } func ...<T: Intervalable where T: ForwardIndexType> (start: T, end: T) -> ClosedInterval<T> { return ClosedInterval(start, end) } // x will be a ClosedInterval let x = 1...5 let s = "hello" // y will be a ClosedInterval let y = s.startIndex...s.endIndex
Of course, this is only something the author of the original type can really implement, not something you can do yourself if you personally prefer the default to be the other way around.
And it’s a bit heavy-handed to force every type to conform to a protocol when really all it needs is to be comparable for the interval to work. But I can’t think of another option (even for the implementor) right now.1 If anyone else can think of a good solution, let me know.
-
except possible tinkering with the
Comparable
hierarchy, which is even more restricting since only Apple could do that. ↩