Swift Extension on Generic Struct Based on Properties of Type T

Swift extension on generic struct based on properties of type T

Update: Conditional conformance has been implemented in Swift 4.1,
and your code

struct Blah<T> {
let someProperty: T
}

extension Blah: Equatable where T: Equatable {
static func == (lhs: Blah, rhs: Blah) -> Bool {
return lhs.someProperty == rhs.someProperty
}
}

compiles and works as expected in Xcode 9.3.


What you are looking for is

  • SE-0143 Conditional conformances

(which in turn is part of the "Generics Manifesto").
The proposal has been accepted for Swift 4 but not yet implemented.
From the proposal:

Conditional conformances express the notion that a generic type will conform to a particular protocol only when its type arguments meet certain requirements.

and a prominent example is

extension Array: Equatable where Element: Equatable {
static func ==(lhs: Array<Element>, rhs: Array<Element>) -> Bool { ... }
}

to make arrays of equatable elements equatable, which is not possible
at present. Your example is essentially

struct SomeWrapper<Wrapped> {
let wrapped: Wrapped
}

extension SomeWrapper: Equatable where Wrapped: Equatable {
static func ==(lhs: SomeWrapper<Wrapped>, rhs: SomeWrapper<Wrapper>) -> Bool {
return lhs.wrapped == rhs.wrapped
}
}

from that proposal.

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

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>>

Extension of constructed generic type in Swift

Swift 5.x:

extension Array where Element == Int {

var sum: Int {
reduce(0, +)
}
}

Why can't a generic struct type be inferred by the argument it is initialized with?

You appear to be trying to create class inheritance out of generics. That's not possible. Generics are not dynamically dispatched. That's on purpose and allows a lot more optimizations.

Providing specialized extension over a default implementation using where clauses like you've done here should only be done for performance improvements. If the compiler can prove something about the types (it's a bidirectional collection rather than a sequence for example), then it can be useful to provide a more efficient algorithm to produce the same output. But all calls to MyStruct.go() should have the same semantics (produce the same output). The decision on what version of go to call is made at compile time based only on information available at compile time. It's possible that test() would be called from some other part of the program with a different type, so the function can't be specialized to apply the right where clause. It has to assume the most general case allowed.

In this specific case, what would you expect to happen if I added the following line:

extension Apple: FromSource_B {}

That's completely legal since Apple conforms to FromSource_B. I could even add this line of code in another module (after everything here has been compiled). So what code should run? This is pointing to a design mistake.

Instead of trying to recreate class-inheritance overriding, what you probably want here is to attach behavior to the Entity types. For example:

// Entities have a way to go()
protocol Entity {
static func go()
}

// And if they don't provide one, there's a default
extension Entity {
static func go() {
print("MyStruct default go()")
}
}

// FromSource_A and _B provide their own default ways to conform

protocol FromSource_A: Entity { }
protocol FromSource_B: Entity { }

extension FromSource_A {
static func go() {
print("MyStruct extension where T : FromSource_A")
}
}

extension FromSource_B {
static func go() {
print("MyStruct extension where T : FromSource_B")
}
}

// Apple and Orange conform, and take the default behaviors (they could provide their own)
struct Apple: FromSource_A { }
struct Orange: FromSource_B { }

// MyStruct (there's no need for a protocol) accepts a GenericArgument, but
// only to nail down what `T` is.
struct GenericArgument<T: Entity> { }

struct MyStruct<T: Entity> {
var genericArgument: GenericArgument<T>

func go () {
T.go()
}
}

// And the rest
func test<T: Entity> (argument: GenericArgument<T>) {
let myStruct = MyStruct<T>(genericArgument: argument)
myStruct.go()
}

let genericArgument = GenericArgument<Apple>()

test(argument: genericArgument) // MyStruct extension where T : FromSource_A

You still need to be a little careful with this. There are cases where it could break. For example, what if someone writes this code in another module:

extension Apple {
static func go() { print("This is an Apple.") }
}

This might or might not behave as you expect. I'd work hard to get rid of all the generics, and almost all the protocols, and do it this way with simple structs and trivial protocols:

protocol Entity {}

protocol Source {
func go()
func makeEntity() -> Entity
}

struct Apple: Entity { }
struct Orange: Entity { }

struct Source_A: Source {
func go() { print("From A") }
func makeEntity() -> Entity { return Apple() }
}

struct Source_B: Source {
func go() { print("From B") }
func makeEntity() -> Entity { return Orange() }

}

struct GenericArgument {
let source: Source
}

struct MyStruct {
var genericArgument: GenericArgument

func go () {
genericArgument.source.go()
}
}

func test(argument: GenericArgument) {
let myStruct = MyStruct(genericArgument: argument)
myStruct.go()
}

let genericArgument = GenericArgument(source: Source_A())

test(argument: genericArgument)

It's possible your problem actually needs generics here, but you should start with writing out the code as simply as possible (including allowing it to duplicate), and then look for how to remove that duplication with generics. You should not jump to generics too quickly; most of us will choose the wrong abstraction.

Swift: Same-Type requirement makes generic parameters equivalent?

It seems like type erasure is the answer to your problem. The key idea to the type erasure pattern is to put your strongly typed but incompatible data (like an AItem<String> and an AItem<Data>) inside of another data structure which stores them with "less precise" types (usually Any).

A major drawback of type erasure is that you're discarding type information—if you need to recover it later on to figure out what you need to do with each element in your array, you'll need to try to cast your data to each possible type, which can be messy and brittle. For this reason, I've generally tried to avoid it where possible.

Anyways, here's an example of type erasure based on your pseudo code:

protocol ConstraintProtocol {}

extension String: ConstraintProtocol{}
extension Data: ConstraintProtocol{}
extension Int: ConstraintProtocol{}

struct AItem<T: ConstraintProtocol> {
var aPara: T

init(aPara: T) {
self.aPara = aPara
}
}

struct AnyAItem {
// By construction, this is always some kind of AItem. The loss of type
// safety here is one of the costs of the type erasure pattern.
let wrapped: Any

// Note: all the constructors always initialize `wrapped` to an `AItem`.
// Since the member variable is constant, our program is "type correct"
// even though type erasure isn't "type safe."
init<T: ConstraintProtocol>(_ wrapped: AItem<T>) {
self.wrapped = wrapped
}

init<T: ConstraintProtocol>(aPara: T) {
self.wrapped = AItem(aPara: aPara);
}

// Think about why AnyAItem cannot expose any properties of `wrapped`...
}

var anArray: [AnyAItem] = [
AnyAItem(aPara: "String"),
AnyAItem(aPara: 1234),
AnyAItem(aPara: "a path".data(using: .utf8)!)
]

for curItem in anArray {
let result = handleItem(item: curItem)
print("result = \(result)")
}

// Note that this function is no longer generic. If you want to try to "recover"
// the type information you erased, you will have to do that somewhere. It's up
// to you where you want to do this.
func handleItem(item: AnyAItem) -> String {
if (item.wrapped is AItem<String>) {
return "String"
} else if (item.wrapped is AItem<Data>) {
return "Data"
} else if (item.wrapped is AItem<Int>) {
return "Int"
}
return "unknown"
}

An alternative to type erasure you could consider, which works well if there's a small, finite set of concrete types your generic could take on, would be to use an enum with associated values to define a "sum type". This might not be a good choice if the protocol you're interested in is from a library that you can't change. In practice, the sum type might look like this:

enum AItem {
case string(String)
case data(Data)
case int(Int)
}

var anArray: [AItem] = [
.string("String"),
.int(1234),
.data("a path".data(using: .utf8)!)
]

for curItem in anArray {
let result = handleItem(item: curItem)
print("result = \(result)")
}

func handleItem(item: AItem) -> String {
// Note that no casting is required, and we don't need an unknown case
// because we know all types that might occur at compile time!
switch item {
case .string: return "String"
case .data: return "Data"
case .int: return "Int"
}
}


Related Topics



Leave a reply



Submit