One of the great feature introduced by Swift 2.0 is possibility to define generic functions for type of Array.
Array is container for values of some type. The type is known upfront. It's called Element
now.
what?
Array is a struct with element and is defined like this:
struct Array<Element>
my goal is to create function that apply to array of integers, and integers only, specifically Int
, not UInt
, not Int32
, not to array of string, or any other type. It has to be array of integer. This is my array:
let array:[Int] = [1,2,3,4,5]
now add function printValues()
using extension:
Note: The implementation of function printValues() is not important at this point. I put "Hello world!" inside.
extension Array where Element: Int { // error
func printValues() {
print("Hello world!")
}
}
though, this code raises error:
error: type
Element
constrained to non-protocol typeInt
why?
Int
is not a protocol, and can't be used with this definition. Kind of unexpected, but consistent with Type Constraint Syntax. At this point let's check the documentation:
"You write type constraint by placing a single class or protocol constraint after a type parameter's name, separated by a colon, as part of the type parameter list."
I especially emphasized here two terms: class and protocol, remember this for the following steps.
The problem is that Int
is a struct, neither class nor protocol, and this is why can't be used in my definition to describe Element
.
What if I add protocol, and make Int
conform to that protocol? yes, that will do the trick.
protocol _IntType { }
extension Int: _IntType {}
from now on I can use _IntType
to describe Int
in constraints. Pretty handy:
extension Array where Element: _IntType {
func printValues() {
print("Hello world!")
}
}
and now I can do this:
let array:[Int] = [1,2,3,4,5]
array.printValues() // Hello world!
how?
values
Let's go deeper. I will try to print all odd values of the array with function printOddValues()
extension Array where Element: _IntType {
func printOddValues() {
for i in self where i % 2 != 0 { // error
print(i, terminator: ",")
}
}
}
error: binary operator
%
cannot be applied to operands of typeElement
andInt
Error is not especially helpful (though, informative enough). I know that Element
is Int
because of generic constraints. BUT... in fact, _IntType
is just the protocol, as such, more than one other type may conform to that protocol. As a conclusion at this point, it is not universally true that _IntType
is just for type Int
(however it is in fact). Because this statement may not be true, it is evaluated as false (uff).
It's not really useful if I can't use values in my functions that apply to this type of array, is it? Fortunately it can be done.
One of the solution is force cast to known type. Not generic approach, but hey, I know that Element
is type of Int
so I can force and tell compiler to use my value as Int
:
for i in self where (i as! Int) % 2 != 0 { /* */ }
quite straightforward, though not especially pretty. In addition it looks like it breaks some layer of abstraction.
value of type
Another situation is when I want to use my array (from generic extension) as parameter for another function. The problem is again: Element
for given:
extension Array where Element: _IntType {
func sum() -> Int {
return calculateSum(self) // error
}
}
func calculateSum(ints: [Int]) -> Int {
return ints.reduce(0, combine: +)
}
error: cannot convert value of type
[Element]
to expected argument type[Int]
"Ah yeah, you'd need to call it something other than value to avoid colliding with that"[1] - not obvious. Apparently value
is taken.
I'll box it with intValue
protocol _IntType {
var intValue: Int { get }
}
extension Int: _IntType {
var intValue: Int {
return Int(self.value)
}
}
extension Array where Element: _IntType {
func sum() -> Int {
return calculateSum(self.map { return $0.intValue }) // $?!?@?@?!!!
}
}
mindf****. Especially way to get values and pass to another function is far, faaaar from readability principles. Iterate over values just to get the value and return as Int
, not Element
really blow my mind:
self.map { return $0.intValue } // [Int]
arrayValue
Another way to deal with this is define "wrapper" around self that returns known type (Self) in place of Element. It looks hacky, it feels hacky, but it's working and is a bit easier to use:
protocol _IntType {
static func arrayValue(array: [Self]) -> [Int] // here Int!
}
extension _IntType {
static func arrayValue(array: [Self]) -> [Self] {
return array
}
}
extension Int: _IntType { }
protocol define arrayValue()
then extension to protocol _IntType
implements wrapper. In the last line Int
conforms to _IntType
with wrapper.
usage:
extension Array where Element: _IntType {
func printOddValues() {
for i in Element.arrayValue(self) where i % 2 != 0 {
print(i, terminator: ",")
}
}
func sum() -> Int {
return calculateSum(Element.arrayValue(self))
}
}
yes, it's kind of complicated, smells like hack, though it just a workaround ;-)
[1,2,3,4,5].sum() // 15
[1,2,3,4,5].printOddValues() // "1,3,5,"
One more thing
one more thing is that it all can[2] be build for _ArrayType
, not Array. _ArrayType
is the protocol Array conforms to, then extend ArrayType (not Array).
protocol ArrayType: _ArrayType {
func arrayValue() -> [Generator.Element]
}
extension Array: ArrayType {
func arrayValue() -> [Generator.Element] {
return self
}
}
extension ArrayType where Generator.Element == Int {
func printOddValues() {
for i in arrayValue() where i % 2 != 0 {
print(i, terminator: ",")
}
}
func sum() -> Int {
return calculateSum(arrayValue())
}
}
and that's good. However.... it's getting tricky, hard, and it brings the tears if I expect typical array operations. This is because _ArrayType is general top level type, while most of the functions to work with Array is implemented by Array
, ArraySlice
and ContinousArray
.
Shortcut using IntegerLiteralType
Worth mention shortcut. Since I already have type IntegerType
I can use it to mark "all integers", then specity one type in particular with constraints on
Element.IntegerLiteralType == UInt8
extension:
extension Array where Element: IntegerType, Element.IntegerLiteralType == UInt8 {
// for Array<UInt8>
}
voila
Conclusions
It is possible to define and use functions for array of specific type. I wish it was easier.
PS
If you wonder why I didn't use IntegerType
, here's my answer: Swift: Madness of Generic Integer. IntegerType is very general protocol and lack many functionality for integers. However most important is that this is just an example, it can be any other type.