On Stack Overflow, user Carpsen90 asked a question about what protocol to use in a generic function such that it could take any number – Int
, Float
, Double
, etc – without having to revert to NSNumber
. This has bitten me a few times, since the integer types (Int
, UInt
, and their kin) conform to the IntegerType
protocol, while Float
and Double
conform to FloatingPointType
, so there’s no shared protocol between them. It’s a little crazy that this isn’t possible right out of the box in Swift, but it does make for a good example of how implementing your own protocols can open up new ways of using generics.
For this post, we’ll implement a minusOneSquared
function that gets a number n
and returns (n - 1)^2
. Our first attempt, without any generic constraints, doesn’t compile:
func
minusOneSquared
<
T
>
(
number
:
T
) -
>
T
{
let
minusOne
=
number
-
1
error: cannot invoke '-' with an argument list of type '(T, IntegerLiteralConvertible)'
return
minusOne
*
minusOne
error: cannot invoke '*' with an argument list of type '(T, T)'
}
To write a generic function like this, we’ll first need to create a new protocol declared with whatever methods and operators we’re going to use in the function. The exact details of what the protocol declares will depend on what the generic function needs to do. Our new protocol, NumericType
, has the arithmetic operators (so we can multiply and subtract) and an initializer that takes an Int
(so we can create a one to subtract from n
).
protocol
NumericType
{
func
+(
lhs
:
Self
,
rhs
:
Self
) -
>
Self
func
-(
lhs
:
Self
,
rhs
:
Self
) -
>
Self
func
*(
lhs
:
Self
,
rhs
:
Self
) -
>
Self
func
/(
lhs
:
Self
,
rhs
:
Self
) -
>
Self
func
%(
lhs
:
Self
,
rhs
:
Self
) -
>
Self
init
(
_
v
:
Int
)
}
All of the numeric types already implement these, but at this point the compiler doesn’t know that they conform to the new NumericType
protocol. We have to make this explicit by using an empty extension – Apple calls this “declaring protocol adoption with an extension.” We’ll do this for Double
, Float
, and all ten integer types:
extension
Double
:
NumericType
{}
extension
Float
:
NumericType
{}
extension
Int
:
NumericType
{}
extension
Int8
:
NumericType
{}
extension
Int16
:
NumericType
{}
extension
Int32
:
NumericType
{}
extension
Int64
:
NumericType
{}
extension
UInt
:
NumericType
{}
extension
UInt8
:
NumericType
{}
extension
UInt16
:
NumericType
{}
extension
UInt32
:
NumericType
{}
extension
UInt64
:
NumericType
{}
Finally, we can write a working function, using the NumericType
protocol as a generic constraint, and call it with any numeric type we like.
func
minusOneSquared
<
T
:
NumericType
>
(
number
:
T
) -
>
T
{
// uses init(_ v: Int) and the - operator
let
minusOne
=
number
-
T
(
1
)
// uses * operator
return
minusOne
*
minusOne
}
-
minusOneSquared
(
5
)
// 16
minusOneSquared
(
2.3
)
// 1.69
minusOneSquared
(
2
as
UInt64
)
// 1