I recently engaged in a conversation on the Swift Users list about converting a JSON array of string values into an enumeration set. I somewhat tongue in cheek suggested that these values be converted into an string-based enumeration, and then their hashValues
be used to set-up flags.
I was, of course, immediately (and rightfully) challenged on whether a solution should hinge on the hashValue
implementation detail — it shouldn’t. But the more I thought about this, I wondered whether it might be reasonable to eliminate potential errors by using the hash values to guide the option set creation.
Here’s what I mean. Consider this enumeration:
private enum LaundryFlags: String { case lowWater, lowHeat, gentleCycle, tumbleDry }
You can use the enumeration to populate an option set, knowing each hashValue
will not overlap, allowing the compiler to choose the implementation details without ever touching them yourself:
public static let lowWater = LaundryOptions(rawValue: 1 << LaundryFlags.lowWater.hashValue)
This approach allows you to build option sets from strings but not worry what the raw values are. Whatever the compiler picks will be consistent:
// String based initialization public init(strings: [String]) { let set: LaundryOptions = strings .flatMap({ LaundryFlags(rawValue: $0) }) // to enumeration .map({ 1 << $0.hashValue }) // to Int, to flag .flatMap({ LaundryOptions(rawValue: $0) }) // to option set .reduce([]) { $0.union($1) } // joined _rawValue = set.rawValue }
There are limits. You cannot use enumerations to represent compound convenience members like the following examples:
public static let energyStar: LaundryOptions = [.lowWater, .lowHeat] public static let gentleStar: LaundryOptions = energyStar.union(gentleCycle)
On the other hand, you can adopt CustomStringConvertible
pretty easily even though raw-value enumerations do not report their members and cannot be initialized from hash values. As the following code shows, it’s not a huge burden to generate a lazy member dictionary. You can boiler plate the implementation and copy/paste your case list into this array:
static var memberDict: Dictionary<Int, String> = [lowWater, lowHeat, gentleCycle, tumbleDry] .reduce([:]) { var dict = $0 dict[$1.hashValue] = "\($1)" return dict }
Reduce the dictionary with bit math from the option set:
public var description: String { let members = LaundryFlags.memberDict.reduce([]) { return (rawValue & 1 << $1.key) != 0 ? $0 + [$1.value] : $0 } return members.joined(separator: ", ") }
So here’s the holy war question: given how simple and reliable this approach is, do I back off of my “never use implementation details” guidance for raw value enumerations and their hash values?
Full gist is here.
One Comment
Hash values are guaranteed to be equal if the values are equal, but there’s no guarantee that they *won’t* be equal if the values are not equal. ‘var hashValue { return 0 }’ is a conforming implementation, for instance. There’s also no guarantee that hashValue is less than the size of an integer, and 1 << hashValue will crash if hashValue overflows the integer.