3

I have a base protocol (Model) that some structs conform to. They also conform to Hashable

protocol Model {}
struct Contact: Model, Hashable {
    var hashValue: Int { return ... }
    static func ==(lhs: Contact, rhs: Contact) -> Bool { return ... } 
}
struct Address: Model, Hashable {
    var hashValue: Int { return ... }
    static func ==(lhs: Address, rhs: Address) -> Bool { return ... } 
}

I have a function that takes an array of objects conforming to Model ([Model]). How can I pass the [Model] to a function that requires Hashables without making Model Hashable?

func complete(with models: [Model]) {
    doSomethingWithHashable(models) //can't do this
}
func doSomethingWithHashable <T:Hashable>(_ objects: [T]) {
    //
}

I'm trying to avoid this

protocol Model: Hashable {}
func complete<T:Model>(with models: [T]) {
    runComparison(models)
}

Because I get "Model cannot be used as a generic constraint..." when I do this

protocol SomethingElse {
    var data: [Model] { get }
}
joels
  • 7,249
  • 11
  • 53
  • 94
  • What if there is another type which conforms to `Model`, but not to `Hashable`? – Martin R Apr 06 '17 at 18:45
  • I don't have that scenario – joels Apr 06 '17 at 18:55
  • 1
    Somewhat related: [Operation on an array of structs implementing Equatable](http://stackoverflow.com/q/41298464/2976878) – you could build an `AnyHashableModel` type-erased wrapper in a similar way to `AnyVehicle` in that Q&A (the main difference being simply storing an extra function for the `hashValue` getting). – Hamish Apr 06 '17 at 19:06
  • That should do it. Can you make your comment into an answer please? – joels Apr 06 '17 at 19:15
  • @joels Sure thing :) – Hamish Apr 06 '17 at 19:34

1 Answers1

2

The problem with your code is that you're talking in terms of Model, which promises nothing about Hashable conformance. As you point out, the problem with telling the compiler about this (i.e deriving Model from Hashable) is you then lose the ability to talk in terms of heterogenous types that conform to Model.

If you don't even care about Model conformance in the first place, you can just use the standard library's AnyHashable type-erased wrapper for completely arbitrary Hashable conforming instances.

However, assuming you do care about Model conformance, you'll have to build your own type-erased wrapper for instances that conform to both Model and Hashable. In my answer here, I demonstrate how a type eraser can be built for Equatable conforming types. The logic there can be very easily extended for Hashable – we just need to store an extra function to return the hashValue of the instance.

For example:

struct AnyHashableModel : Model, Hashable {

    static func ==(lhs: AnyHashableModel, rhs: AnyHashableModel) -> Bool {

        // forward to both lhs's and rhs's _isEqual in order to determine equality.
        // the reason that both must be called is to preserve symmetry for when a
        // superclass is being compared with a subclass.
        // if you know you're always working with value types, you can omit one of them.
        return lhs._isEqual(rhs) || rhs._isEqual(lhs)
    }

    private let base: Model

    private let _isEqual: (_ to: AnyHashableModel) -> Bool
    private let _hashValue: () -> Int

    init<T : Model>(_ base: T) where T : Hashable {

        self.base = base

        _isEqual = {
            // attempt to cast the passed instance to the concrete type that
            // AnyHashableModel was initialised with, returning the result of that
            // type's == implementation, or false otherwise.
            if let other = $0.base as? T {
                return base == other
            } else {
                return false
            }
        }

        // simply assign a closure that captures base and returns its hashValue
        _hashValue = { base.hashValue }
    }

    var hashValue: Int { return _hashValue() }
}

You would then use it like so:

func complete(with models: [AnyHashableModel]) {
    doSomethingWithHashable(models)
}

func doSomethingWithHashable<T : Hashable>(_ objects: [T]) {
    //
}

let models = [AnyHashableModel(Contact()), AnyHashableModel(Address())]
complete(with: models)

Here I'm assuming that you'll also want to use it as a wrapper for Model's requirements (assuming there are some). Alternatively, you can expose the base property and remove the Model conformance from AnyHashableModel itself, making callers access the base for the underlying Model conforming instance:

struct AnyHashableModel : Hashable {
    // ...
    let base: Model
    // ...
}

You will however note that the above type-erased wrapper is only applicable to types that are both Hashable and a Model. What if we want to talk about some other protocol where the conforming instances are Hashable?

A more general solution, as I demonstrate in this Q&A, is to instead accept types that are both Hashable and conform to some other protocol – the type of which is expressed by a generic placeholder.

As there's currently no way in Swift to express a generic placeholder that must conform to a protocol given by another generic placeholder; this relationship must be defined by the caller with a transform closure to perform the necessary upcast. However, thanks to Swift 3.1's acceptance of concrete same-type requirements in extensions, we can define a convenience initialiser to remove this boilerplate for Model (and this can be repeated for other protocol types).

For example:

/// Type-erased wrapper for a type that conforms to Hashable,
/// but inherits from/conforms to a type T that doesn't necessarily require
/// Hashable conformance. In almost all cases, T should be a protocol type.
struct AnySpecificHashable<T> : Hashable {

    static func ==(lhs: AnySpecificHashable, rhs: AnySpecificHashable) -> Bool {
        return lhs._isEqual(rhs) || rhs._isEqual(lhs)
    }

    let base: T

    private let _isEqual: (_ to: AnySpecificHashable) -> Bool
    private let _hashValue: () -> Int

    init<U : Hashable>(_ base: U, upcast: (U) -> T) {

        self.base = upcast(base)

        _isEqual = {
            if let other = $0.base as? U {
                return base == other
            } else {
                return false
            }
        }

        _hashValue = { base.hashValue }
    }
    var hashValue: Int { return _hashValue() }
}

// extension for convenience initialiser for when T is Model.
extension AnySpecificHashable where T == Model {
    init<U : Model>(_ base: U) where U : Hashable {
        self.init(base, upcast: { $0 })
    }
}

You would now want to wrap your instances in a AnySpecificHashable<Model>:

func complete(with models: [AnySpecificHashable<Model>]) {
    doSomethingWithHashable(models)
}

func doSomethingWithHashable<T : Hashable>(_ objects: [T]) {
    //
}

let models: [AnySpecificHashable<Model>] = [
    AnySpecificHashable(Contact()),
    AnySpecificHashable(Address())
]

complete(with: models)
Community
  • 1
  • 1
Hamish
  • 78,605
  • 19
  • 187
  • 280