I have a base protocol (Model) that some structs conform to. They also conform to Hashable
protocol Model {}
struct Contact: Model, Hashable {
var hashVa
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(_ 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(_ 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 : 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(_ 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(_ base: U) where U : Hashable {
self.init(base, upcast: { $0 })
}
}
You would now want to wrap your instances in a AnySpecificHashable
:
func complete(with models: [AnySpecificHashable]) {
doSomethingWithHashable(models)
}
func doSomethingWithHashable(_ objects: [T]) {
//
}
let models: [AnySpecificHashable] = [
AnySpecificHashable(Contact()),
AnySpecificHashable(Address())
]
complete(with: models)