Holy War: Enum Hash Values and Option Sets

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.