Using diff in an array of objects that conform to

2019-07-12 20:32发布

I'm experimenting with using Composition instead of Inheritance and I wanted to use diff on an array of objects that comply with a given protocol.

To do so, I implemented a protocol and made it comply with Equatable:

// Playground - noun: a place where people can play
import XCPlayground
import Foundation

protocol Field:Equatable {
    var content: String { get }
}

func ==<T: Field>(lhs: T, rhs: T) -> Bool {
    return lhs.content == rhs.content
}

func ==<T: Field, U: Field>(lhs: T, rhs: U) -> Bool {
    return lhs.content == rhs.content
}

struct First:Field {
    let content:String
}

struct Second:Field {
    let content:String
}

let items:[Field] = [First(content: "abc"), Second(content: "cxz")] //                 

标签: swift lcs
2条回答
时光不老,我们不散
2楼-- · 2019-07-12 20:53

The problem comes from a combination of the meaning of the Equatable protocol and Swift’s support for type overloaded functions.

Let’s take a look at the Equatable protocol:

protocol Equatable
{
    static func ==(Self, Self) -> Bool
}

What does this mean? Well it’s important to understand what “equatable” actually means in the context of Swift. “Equatable” is a trait of a structure or class that make it so that any instance of that structure or class can be compared for equality with any other instance of that structure or class. It says nothing about comparing it for equality with an instance of a different class or structure.

Think about it. Int and String are both types that are Equatable. 13 == 13 and "meredith" == "meredith". But does 13 == "meredith"?

The Equatable protocol only cares about when both things to be compared are of the same type. It says nothing about what happens when the two things are of different types. That’s why both arguments in the definition of ==(::) are of type Self.

Let’s look at what happened in your example.

protocol Field:Equatable 
{
    var content:String { get }
}

func ==<T:Field>(lhs:T, rhs:T) -> Bool 
{
    return lhs.content == rhs.content
}

func ==<T:Field, U:Field>(lhs:T, rhs:U) -> Bool 
{
    return lhs.content == rhs.content
}

You provided two overloads for the == operator. But only the first one has to do with Equatable conformance. The second overload is the one that gets applied when you do

First(content: "abc") == Second(content: "abc")

which has nothing to do with the Equatable protocol.

Here’s a point of confusion. Equability across instances of the same type is a lower requirement than equability across instances of different types when we’re talking about individually bound instances of types you want to test for equality. (Since we can assume both things being tested are of the same type.)

However, when we make an array of things that conform to Equatable, this is a higher requirement than making an array of things that can be tested for equality, since what you are saying is that every item in the array can be compared as if they were both of the same type. But since your structs are of different types, you can’t guarantee this, and so the code fails to compile.

Here’s another way to think of it.

Protocols without associated type requirements, and protocols with associated type requirements are really two different animals. Protocols without Self basically look and behave like types. Protocols with Self are traits that types themselves conform to. In essence, they go “up a level”, like a type of type. (Related in concept to metatypes.)

That’s why it makes no sense to write something like this:

let array:[Equatable] = [5, "a", false]

You can write this:

let array:[Int] = [5, 6, 7]

or this:

let array:[String] = ["a", "b", "c"]

or this:

let array:[Bool] = [false, true, false]

Because Int, String, and Bool are types. Equatable isn’t a type, it’s a type of a type.

It would make “sense” to write something like this…

let array:[Equatable] = [Int.self, String.self, Bool.self]

though this is really stretching the bounds of type-safe programming and so Swift doesn’t allow this. You’d need a fully flexible metatyping system like Python’s to express an idea like that.

So how do we solve your problem? Well, first of all realize that the only reason it makes sense to apply SwiftLCS on your array is because, at some level, all of your array elements can be reduced to an array of keys that are all of the same Equatable type. In this case, it’s String, since you can get an array keys:[String] by doing [Field](...).map{ $0.content }. Perhaps if we redesigned SwiftLCS, this would make a better interface for it.

However, since we can only compare our array of Fields directly, we need to make sure they can all be upcast to the same type, and the way to do that is with inheritance.

class Field:Equatable
{
    let content:String

    static func == (lhs:Field, rhs:Field) -> Bool
    {
        return lhs.content == rhs.content
    }

    init(_ content:String)
    {
        self.content = content
    }
}

class First:Field
{
    init(content:String)
    {
        super.init(content)
    }
}

class Second:Field
{
    init(content:String)
    {
        super.init(content)
    }
}


let items:[Field] = [First(content: "abc"), Second(content: "cxz")]

The array then upcasts them all to type Field which is Equatable.

By the way, ironically, the “protocol-oriented” solution to this problem actually still involves inheritance. The SwiftLCS API would provide a protocol like

protocol LCSElement 
{
    associatedtype Key:Equatable
    var key:Key { get }
}

We would specialize it with a superclass

class Field:LCSElement
{
    let key:String // <- this is what specializes Key to a concrete type

    static func == (lhs:Field, rhs:Field) -> Bool
    {
        return lhs.key == rhs.key
    }

    init(_ key:String)
    {
        self.key = key
    }
}

and the library would use it as

func LCS<T: LCSElement>(array:[T])
{
    array[0].key == array[1].key
    ...
}

Protocols and Inheritance are not opposites or substitutes for one another. They complement each other.

查看更多
对你真心纯属浪费
3楼-- · 2019-07-12 20:59

I know this is probably now what you want but the only way I know how to make it work is to introduce additional wrapper class:

struct FieldEquatableWrapper: Equatable {
    let wrapped: Field

    public static func ==(lhs: FieldEquatableWrapper, rhs: FieldEquatableWrapper) -> Bool {
        return lhs.wrapped.content == rhs.wrapped.content
    }

    public static func diff(_ coll: [Field], _ otherCollection: [Field]) -> Diff<Int> {
        let w1 = coll.map({ FieldEquatableWrapper(wrapped: $0) })
        let w2 = otherCollection.map({ FieldEquatableWrapper(wrapped: $0) })
        return w1.diff(w2)
    }
}

and then you can do

    let outcome = FieldEquatableWrapper.diff(array1, array2)

I don't think you can make Field to conform to Equatable at all as it is designed to be "type-safe" using Self pseudo-class. And this is one reason for the wrapper class. Unfortunately there seems to be one more issue that I don't know how to fix: I can't put this "wrapped" diff into Collection or Array extension and still make it support heterogenous [Field] array without compilation error:

using 'Field' as a concrete type conforming to protocol 'Field' is not supported

If anyone knows a better solution, I'm interested as well.

P.S.

In the question you mention that

print(First(content: "abc") == Second(content: "abc")) // false

but I expect that to be true given the way you defined your == operator

查看更多
登录 后发表回答