The exercise was to write my own map()
function over Collection
(without using any functional primitives, such as reduce()
). It should handle a case such as this:
func square(_ input: Int) -> Int {
return input * input
}
let result = input.accumulate(square) // [1,2,3] => [1,4,9]
My first attempt was:
extension Collection {
func accumulate(_ transform: (Element) -> Element) -> [Element] {
var array: [Element] = []
for element in self {
array.append(transform(element))
}
return array
}
}
This works fine in a playground, but fails to build against the tests, giving an error:
Value of type '[Int]' has no member 'accumulate'
The solution is to genericize the accumulate
method:
extension Collection {
func accumulate<T>(_ transform: (Element) -> T) -> [T] {
var array: [T] = []
for element in self {
array.append(transform(element))
}
return array
}
}
I recognize that the generic version is less restrictive (doesn't require the transform to return same type), but given that the tests don't require this generality, why does the compiler?
Out of curiousity I tried:
extension Collection {
func accumulate<Element>(_ transform: (Element) -> Element) -> [Element] {
var array: [Element] = []
for element in self {
array.append(transform(element))
}
return array
}
}
which throws the fascinating build error: '(Self.Element) -> Element' is not convertible to '(Element) -> Element'
at the append()
statement.
So the compiler (of course) knows that the first Element is Self.Element, but doesn't treat the other Element type as the same. Why?
UPDATE:
Based on the answers, it appears that the rejection of the first version was a compiler bug, fixed in XCode 9.2 (I'm on 9.1).
But still I wondered whether in
func accumulate(_ transform: (Element) -> Element) -> [Element]
it would see two types (Self.Element
and Element
) or recognize that they're the same.
So I did this test:
let arr = [1,2,3]
arr.accumulate {
return String(describing: $0)
}
Sure enough, got the expected error: error: cannot convert value of type 'String' to closure result type 'Int'
So the correct answer is: the compiler will treat references to Element as the same, as long as there isn't a generic type that overloads the name.
Oddly, though, this succeeds:
[1,2,3].accumulate {
return String(describing: $0)
}
PS. Thanks to everyone for your input! The bounty was auto-awarded.
Code that uses generics has many benefits over non-generic code: Stronger type checks at compile time. A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find.
Better performance. Generic collection types generally perform better for storing and manipulating value types because there is no need to box the value types. Generic delegates enable type-safe callbacks without the need to create multiple delegate classes.
So, anything that is used as generics has to be convertable to Object (in this example get(0) returns an Object ), and the primitive types aren't. So they can't be used in generics.
Generics allow you to define the specification of the data type of programming elements in a class or a method, until it is actually used in the program. In other words, generics allow you to write a class or method that can work with any data type.
The original build error was a compiler error. In fact, the compiler will recognize that all instances of Element
are the same, as long Element
hasn't been overloaded as a generic type on the function.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With