Thursday, December 14, 2017

Why is generic required when all types are already defined?

Leave a Comment

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!

5 Answers

Answers 1

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.

Answers 2

  • About the first question, working with Xcode 9.2 and Swift 4 I didn't receive any building error such as:

Value of type '[Int]' has no member 'accumulate'

so doing this:

var mystuff:[Int] = [1,2,3] let result = mystuff.accumulate(square) 

it's just giving me the correct result [1,4,9]

  • For the second question, the function prototype is wrong, you should try this (with Self.Element):

    extension Collection {   func accumulate<Element>(_ transform: (Self.Element) -> Element) -> [Element] {     var array: [Element] = []     for element in self {       array.append(transform(element))     }     return array    } } 

Answers 3

I'll focus on your second question.

func accumulate<Element>(_ transform: (Element) -> Element) -> [Element] 

The trouble with writing this signature is you've got two different types with the same name.

  • The first type is Element which is your generic type (the bit between the angle brackets).
  • The second type is Self.Element, that is, the type of the elements in your collection, declared by the protocol itself. Usually you don't have to explicitly write the Self. part, but because your generic type has the same name as this one, Swift can't tell them apart otherwise.

The difference is more obvious if you change the name of the generic type:

func accumulate<E>(_ transform: (E) -> E) -> [E] 

This is equivalent to the accumulate<Element> version — changing the name simply highlights what's actually happening.

In a more general sense, Swift will let you name your types whatever you wish. But if a types' name conflicts with another type from a different scope, then you either have to disambiguate it. If you don't disambiguate, Swift will choose the most-local match. In your function, the generic type is "most-local".

Imagine you were to define your own String type:

struct String {   // ... } 

This is totally valid, but if you want to then use the String type provided by the Swift standard lib, you have to disambiguate like this:

let my_string: String = String() let swift_string: Swift.String = "" 

And this is the reason why Andrea changed the signature of the function. You need to tell the compiler which "Element" type you're referring to.

func accumulate<Element>(_ transform: (Self.Element) -> Element) -> [Element] 

In general I would advise against having a generic type match the name of another type you're using. It's just confusing for everyone. 😄

Answers 4

I'm not so sure about your first question. If it works in the playground, but not in your tests, my first guess would be to make the function public. Tests are usually defined in separate modules, and to make something visible in another module, it should be declared public.

extension Collection {   public func accumulate //... } 

Answers 5

When implementing an extension to a Collection, the associated type of the collection would be by default Element; It would be confusing to name your generic as "Element" (func accumulate<Element>), however, for your case, there is even no need to declare your method signature as follows:

func accumulate<Element>(_ transform: (Self.Element) -> Element) -> [Element] 

Instead, it shall be:

func accumulate(_ transform: (Element) -> Element) -> [Element] 

Also, if you are aiming to let your method to be functional only for integers, you should constraint your extension to be applicable only for collections of BinaryInteger, as follows:

extension Collection where Element: BinaryInteger {     func accumulate(_ transform: (Element) -> Element) -> [Element] {          var array: [Element] = []         for element in self {             array.append(transform(element))         }         return array     } } 

Or for expanding the scope, it could be Numeric instead.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment