Below are two examples of what one might assume to be gaps / missing protocols in the current standard library (Swift 2, Xcode 7 beta 2). I'd like to get advice on how to handle the particular situation described here as well as similar situations in general (ie how to handle the prevalence and successive diminishing of supposed gaps during the rapid evolution of Swift and its standard library).
Ok. Consider the primitive number types in the standard library (Double, Float, Float80, UInt, Int, UInt8, Int8, UInt16 ... Int64). They share some common characteristics, some of which are explicitly available for us to use through protocols such as IntegerArithmeticType, FloatingPointType, IntegerType etc, and others which are only there implicitly.
Two examples of such only-implicitly common features / missing protocols:
- The fact that any primitive number type can be (explicitly) converted to (initialized with) any other primitive number type.
- All the integer types have bounds specified by their .max and .min properties.
In other words, these common groupings are there, it's just that they haven't been explicitly declared as such, ie there's currently nothing like a "PrimitiveNumberTypeConvertible" or "BoundedType" protocol in the standard library, for some reason.
Here's a contrived example of why one might feel the need for and write these two protocols:
Let's say we want to be able to clamp any Comparable. This is easy to do without having to fill any gaps in the standard library:
extension Comparable {
func clampedBetween(minValue: Self, and maxValue: Self) -> Self {
return self < minValue ? minValue : self > maxValue ? maxValue : self
}
}
Nice!
Next, we want to have a flexible way of clamping any primitive floating point value to the bounds of any primitive integer type, ie we want to be able to do the following for any floating point value f:
let fClampedToByteBounds = f.clampedToBoundsOf(UInt8)
let fClampedToInt64Bounds = f.clampedToBoundsOf(Int64)
So we will extend the floating point types with a .clampedToBoundsOf method, something like this:
extension PrimitiveNumberTypeConvertible where Self: FloatingPointType {
func clampedToBoundsOf<T: BoundedIntegerType>(_: T.Type) -> Self {
return self.clampedBetween(Self(T.min), and: Self(T.max))
}
}
This, however, requires us to fill the above mentioned gaps / write the missing protocols. For example like this:
protocol PrimitiveNumberTypeConvertible {
init()
init(_ other: Double)
init(_ other: Float)
init(_ other: Float80)
init(_ other: UInt8)
init(_ other: UInt16)
init(_ other: UInt32)
init(_ other: UInt64)
init(_ other: Int8)
init(_ other: Int16)
init(_ other: Int32)
init(_ other: Int64)
init(_ other: UInt)
init(_ other: Int)
init<T : PrimitiveNumberTypeConvertible>(_ other: T)
}
extension PrimitiveNumberTypeConvertible {
init<T : PrimitiveNumberTypeConvertible>(_ other: T) {
switch other {
case let o as Double: self.init(o)
case let o as Float: self.init(o)
case let o as Float80: self.init(o)
case let o as UInt8: self.init(o)
case let o as UInt16: self.init(o)
case let o as UInt32: self.init(o)
case let o as UInt64: self.init(o)
case let o as UInt: self.init(o)
case let o as Int8: self.init(o)
case let o as Int16: self.init(o)
case let o as Int32: self.init(o)
case let o as Int64: self.init(o)
case let o as Int: self.init(o)
default: fatalError("Add case clause for \(T.self) in \(__FUNCTION__)")
}
}
}
extension Double: PrimitiveNumberTypeConvertible {}
extension Float: PrimitiveNumberTypeConvertible {}
extension Float80: PrimitiveNumberTypeConvertible {}
extension UInt8: PrimitiveNumberTypeConvertible {}
extension UInt16: PrimitiveNumberTypeConvertible {}
extension UInt32: PrimitiveNumberTypeConvertible {}
extension UInt64: PrimitiveNumberTypeConvertible {}
extension UInt: PrimitiveNumberTypeConvertible {}
extension Int8: PrimitiveNumberTypeConvertible {}
extension Int16: PrimitiveNumberTypeConvertible {}
extension Int32: PrimitiveNumberTypeConvertible {}
extension Int64: PrimitiveNumberTypeConvertible {}
extension Int: PrimitiveNumberTypeConvertible {}
protocol BoundedType {
static var max: Self { get }
static var min: Self { get }
}
extension UInt8 : BoundedType {}
extension UInt16 : BoundedType {}
extension UInt32 : BoundedType {}
extension UInt64 : BoundedType {}
extension UInt : BoundedType {}
extension Int8 : BoundedType {}
extension Int16 : BoundedType {}
extension Int32 : BoundedType {}
extension Int64 : BoundedType {}
extension Int : BoundedType {}
typealias BoundedIntegerType = protocol <BoundedType, IntegerType, PrimitiveNumberTypeConvertible>
// Ok, we are done with writing the missing protocols, now we can write the code we wanted:
extension Comparable {
func clampedBetween(minValue: Self, and maxValue: Self) -> Self {
return self < minValue ? minValue : self > maxValue ? maxValue : self
}
}
extension PrimitiveNumberTypeConvertible where Self: FloatingPointType {
func clampedToBoundsOf<T: BoundedIntegerType>(_: T.Type) -> Self {
return self.clampedBetween(Self(T.min), and: Self(T.max))
}
}
Is this sort of thing considered good or bad practice? Perhaps there are very good reasons why those two protocols have been consciously left out / never considered for inclusion in the std lib? How are we to know? Are there any best practices / guidelines regarding this? Or some overview of the philosophy and direction in which the standard library is heading?