Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: Extension on [<SomeType<T>?] to produce [<T>?] possible?

In Swift, I have a custom struct with this basic premise:

A wrapper struct that can contain any type that conforms to BinaryInteger such as Int, UInt8, Int16, etc.

protocol SomeTypeProtocol {
    associatedtype NumberType

    var value: NumberType { get set }
}

struct SomeType<T: BinaryInteger>: SomeTypeProtocol {
    typealias NumberType = T

    var value: NumberType
}

And an extension on Collection:

extension Collection where Element: SomeTypeProtocol {
    var values: [Element.NumberType] {
        return self.map { $0.value }
    }
}

For example, this works nicely:

let arr = [SomeType(value: 123), SomeType(value: 456)]

// this produces [123, 456] of type [Int] since literals are Int by default
arr.values

What I would like to do is the exact same thing, but for SomeType<T>?

let arr: [SomeType<Int>?] = [SomeType(value: 123), SomeType(value: 456)]

// this doesn't work, obviously
arr.values

// but what I want is this:
arr.values // to produce [Optional(123), Optional(456)]

I've tried numerous attempts to solve this and a fair amount of research, but I'm hoping any of the sage Swift veterans might shed some light on this.

This is what I envision it might look like, but this doesn't work:

extension Collection where Element == Optional<SomeType<T>> {
    var values: [T?] {
        return self.map { $0?.value }
    }
}

This is a clumsy way of achieving the goal without using generics, and it works:

extension Collection where Element == Optional<SomeType<Int>> {
    var values: [Int?] {
        return self.map { $0?.value }
    }
}

let arr: [SomeType<Int>?] = [SomeType(value: 123), SomeType(value: 456)]
arr.values // [Optional(123), Optional(456)]

But it requires manually writing extensions for every known type conforming to BinaryInteger, and will not automatically include possible future types adopting BinaryInteger without manually updating the code.

// "..." would contain the var values code from above, copy-and-pasted
extension Collection where Element == Optional<SomeType<Int>> { ... }
extension Collection where Element == Optional<SomeType<Int8>> { ... }
extension Collection where Element == Optional<SomeType<UInt8>> { ... }
extension Collection where Element == Optional<SomeType<Int16>> { ... }
extension Collection where Element == Optional<SomeType<UInt16>> { ... }
extension Collection where Element == Optional<SomeType<Int32>> { ... }
extension Collection where Element == Optional<SomeType<UInt32>> { ... }
extension Collection where Element == Optional<SomeType<Int64>> { ... }
extension Collection where Element == Optional<SomeType<UInt64>> { ... }

EDIT 2018-Jun-23:

Solution #1 - Fully Generic but Must be Func, Not Computed Property

Expanded on Ole's reply:

Pros: If values() becomes a func and not a computed property, this is an elegant solution.

Cons: No known way to implement this approach as computed properties, and Swift's Quick Help popup shows [T] and [T?] when inspecting values() in your code. ie: it just says func values<T>() -> [T] where T : BinaryInteger which isn't very informative or Swifty. However it still remains strongly typed of course.

extension Collection {
    func values<T>() -> [T] where Element == SomeType<T> {
        return map { $0.value }
    }

    func values<T>() -> [T?] where Element == SomeType<T>? {
        return map { $0?.value }
    }
}

Solution #2 - Optional Protocol Workaround

Expanded on Martin's reply:

Pros: Allows use of computed properties (cleaner for the end-user to access since it doesn't require func parens) and shows inferred type in Xcode's Quick Help popup.

Cons: Not as elegant from an internal code standpoint, as it requires a workaround. But not necessarily a drawback.

// Define OptionalType

protocol OptionalType {
    associatedtype Wrapped
    var asOptional: Wrapped? { get }
}

extension Optional: OptionalType {
    var asOptional: Wrapped? {
        return self
    }
}

// Extend Collection

extension Collection where Element: SomeTypeProtocol {
    var values: [Element.NumberType] {
        return self.map { $0.value }
    }
}

extension Collection where Element: OptionalType, Element.Wrapped: SomeTypeProtocol {
    var values: [Element.Wrapped.NumberType?] {
        return self.map { $0.asOptional?.value }
    }
}
like image 324
stef Avatar asked Jun 23 '18 09:06

stef


People also ask

Can you add a stored property to a type by using an extension?

As you may know Swift does not allow stored properties into extensions. That's by design: “Extensions may not contain stored properties.”

Can we extend protocol in Swift?

In Swift, you can even extend a protocol to provide implementations of its requirements or add additional functionality that conforming types can take advantage of. For more details, see Protocol Extensions. Extensions can add new functionality to a type, but they can't override existing functionality.

How do I use Swift 5 extension?

Creating an extension in Swift Creating extensions is similar to creating named types in Swift. When creating an extension, you add the word extension before the name. extension SomeNamedType { // Extending SomeNamedType, and adding new // functionality to it. }

How do you name a file extension in Swift?

Naming a fileAll Swift files should use the . swift extension. The name of a file should make it clear what the contents are. For example, if there is a single class named EasyTapButton then the file should be called EasyTapButton.


2 Answers

Martin R's answer is a good solution. An alternative that doesn't require an extra marker protocol is this: write an unconstrained extension on Collection, and in that extension, define a generic function that's constrained to where Element == SomeType<T>?:

extension Collection {
    func values<T>() -> [T?] where Element == SomeType<T>? {
        return map( { $0?.value })
    }
}

This works:

let arr: [SomeType<Int>?] = [SomeType(value: 123), SomeType(value: 456)]
arr.values() // [Optional(123), Optional(456)]

You'll notice that I used a func instead of a computed property. I couldn't get the generic syntax right. Isn't this supposed to work?

extension Collection {
    // error: consecutive declarations on a line must be separated by ';'
    var values<T>: [T?] where Element == SomeType<T>? {
        return self.map( { $0?.value })
    }
}
like image 112
Ole Begemann Avatar answered Sep 17 '22 21:09

Ole Begemann


I don't know if there is a simpler solution now, but you can use the same “trick” as in How can I write a function that will unwrap a generic property in swift assuming it is an optional type? and Creating an extension to filter nils from an Array in Swift, the idea goes back to this Apple Forum Thread.

First define a protocol to which all optionals conform:

protocol OptionalType {
    associatedtype Wrapped
    var asOptional: Wrapped? { get }
}

extension Optional : OptionalType {  
    var asOptional: Wrapped? {  
        return self 
    }  
}  

Now the desired extension can be defined as

extension Collection where Element: OptionalType, Element.Wrapped: SomeTypeProtocol {
    var values: [Element.Wrapped.NumberType?] {
        return self.map( { $0.asOptional?.value })
    }
}

and that works as expected:

let arr = [SomeType(value: 123), nil, SomeType(value: 456)]
let v = arr.values

print(v) // [Optional(123), Optional(456)]
print(type(of: v)) // Array<Optional<Int>>
like image 24
Martin R Avatar answered Sep 21 '22 21:09

Martin R