We are developing a distributable package (framework) and have an enum that looks something like this:
public enum Status: Equatable {
case waiting
case initialized
case failed(Error)
}
We definitely want the enum to be Equatable, we've found that usage of it is much too tedious if not.
Of course conformance to Equatable doesn't just work. So we are looking for ways to make this enum Equatable and still support the associated Error type for the failed case.
One idea that we came up with was to create a protocol "UnconstrainedEquatable":
/// A type that can compare itself to other instances of UnconstrainedEquatable.
/// The conforming types must check to make sure the passed in instance is of the same
/// type as Self.
public protocol UnconstrainedEquatable {
/// Returns a value that specifies if this instance is equal to another instance.
/// - Parameter other: The other instance to compare for equality.
func isEqual(to other: UnconstrainedEquatable) -> Bool
}
public extension UnconstrainedEquatable where Self: Equatable {
/// Provides a default implementation of isEqual that uses Equatable conformance
/// for the heavy lifting.
func isEqual(to other: UnconstrainedEquatable) -> Bool {
guard let other = other as? Self else { return false }
return self == other
}
}
/// Make sure the error types that we may through conform:
extension SomeError: UnconstrainedEquatable, Equatable { }
extension SomeOtherError: UnconstrainedEquatable, Equatable { }
This is a generic approach (UnconstrainedEquatable has nothing to do with Error the way it's defined) and it's usage would then mean our status would look like this, (and we would implement the == manually somewhere below):
public enum Status: Equatable {
case waiting
case initialized
case failed(Error & UnconstrainedEquatable)
}
However, that doesn't quite sit right for some reason. Are we somehow shirking the strongly typed system of swift?
Another idea is to do something similar but more targeted on the Error type:
public protocol BasicError: Error {
/// Returns a value that specifies if this instance is equal to another instance.
/// - Parameter other: The other instance to compare for equality.
func isEqual(to other: BasicError) -> Bool
}
public extension BasicError where Self: Equatable {
/// Provides a default implementation of isEqual that uses Equatable conformance
/// for the heavy lifting.
func isEqual(to other: BasicError) -> Bool {
guard let other = other as? Self else { return false }
return self == other
}
}
extension SomeError: BasicError, Equatable { }
extension SomeOtherError: BasicError, Equatable { }
Where the name BasicError can/should be replaced with "Error".
It's usage would then mean our status would look like this:
public enum Status: Equatable {
case waiting
case initialized
case failed(BasicError)
}
But that doesn't sit quite right either. This "BasicError" or "OurProductError" protocol doesn't add anything product specific to the Error protocol, it adds something to help us deal with Equatable in an un-constrained way.
I guess we could name the protocol UnconstrainedEquatableError?
Does anybody have any thoughts on the ideas above? Are there other established ways to solve this?