Brent’s Week 3 Bike Sheed

If you're not following Brent Simmons, then go here first: http://inessential.com/langvalue.

Ok, now that you've read that, you should probably stop reading here until you go and try to solve the problems there. For extra points, only pick one of the struct/enum choices and pick one of the protocol based choices.

I'll wait…


If you've been following along with any of Brent's posts, you might notice that he has (intentionally?) created a problem that will help you uncover some of the limitations of Swift's type system based on some of his own frustrations (see his KVC diary posts). Nonetheless, it is good to compare what options are on the table when a problem like he presented is put before us.

Now, if you've only played with Swift on the surface, you might not know the gotcha's yet. Also, you've probably heard a bit about "PROTOCOLS, PROTOCOLS, PROTOCOLS!". Naturally, you might say to yourself, "AHA! The protocol solution must be the 'right one', 'best one', or the 'easiest one' to implement."

However, if you have been around Swift for a while, I'm sure you noticed that Brent has fiendishly put a requirement of Set<T>.

Meme - Train on bridge falling off, Caption: Use Protocols They Said... It's Awesome They Said...

So there's a dirty little (apparent) problem with protocols today… once they get a Self requirement, hetergenous usage of them goes out the window for any strongly typed collection.

But… maybe this is a sign. Maybe the "protocols first" approach tells us something. After all, implementing the protocol version of Brent's question is actually not even supported today in Swift (option 4 at least).

Solving the Problem

Let me suggest something: the spec artificially makes the problem harder and implies that protocols should be used in a way that I do not think Swift intends you to use them. Protocols are not types, they do not behave like types, and they cannot be used like types.

I think the better option to solving the problem presented is option #5: using enums and protocols.

Enums

The problem statement is very clear: there are three, and only three, different type constructs that are allowed. Namely:

  • Integer
  • String
  • Table

To me, this clearly implies that an enum is going to be the right structure to use.

enum LangValue {
    case IntegerValue(Int)
    case StringValue(String)
    indirect case TableValue([String:LangValue])
}

Beautiful. Seriously, I think that is an extremely clear and precise model of exactly what we want.

Now, we need to add a type function. So the complete, initial model looks like this:

enum LangValueType {
    case Integer
    case String
    case Table
}

enum LangValue {
    case IntegerValue(Int)
    case StringValue(String)
    indirect case TableValue([String:LangValue])

    var type: LangValueType {
        switch self {
        case .IntegerValue(_): return .Integer
        case .StringValue(_): return .String
        case .TableValue(_): return .Table
        }
    }
}

Protocols

OK, now here is where I think protocols should be used. You see, the spec now asks for four different classes of behaviors:

  • Convertible to an Integer
  • Convertible to a String
  • Addable
  • Storable (my term for the dictionary-like operations)

To me, this screams protocols.

protocol IntegerConvertible {
    func integerValue() throws -> Int
}

protocol StringConvertible {
    func stringValue() throws -> String
}

protocol Addable {
    typealias AddableType
    func add(other: AddableType) throws -> AddableType
}

protocol Storeable {
    typealias ValueType
    typealias KeyType

    mutating func set(object object: ValueType, forKey key: KeyType) throws
    mutating func remove(forKey key: KeyType) throws
    func object(forKey key: KeyType) throws -> ValueType?
    func keys() throws -> [KeyType]
}

Notice something: none of these protocols are tied to the LangValue type. If a type is needed, a typealias is used to make this generic so that these protocols can be applied to any type. This is key (and goes a bit against some of my previous recommendations about not making protocols generic).

Now it's simply a matter of applying these protocols to our type:

extension LangValue : IntegerConvertible {
    func integerValue() throws -> Int {
        switch self {
        case let IntegerValue(value): return value
        case let StringValue(value): return (value as NSString).integerValue
        default: throw LangCoercionError.InvalidInteger
        }
    }
}

extension LangValue : StringConvertible {
    func stringValue() throws -> String {
        switch self {
        case let IntegerValue(value): return NSString(format: "%d", value) as String
        case let StringValue(value): return value
        default: throw LangCoercionError.InvalidString
        }
    }
}

extension LangValue : Addable {
    func add(other: LangValue) throws -> LangValue {
        switch (self, other) {
        case let (.IntegerValue(lvalue), .IntegerValue(rvalue)):
            return .IntegerValue(lvalue + rvalue)

        case (.StringValue(_), .IntegerValue(_)): fallthrough
        case (.IntegerValue(_), .StringValue(_)): fallthrough
        case (.StringValue(_), .StringValue(_)):
            return try LangValue.StringValue(self.stringValue() + other.stringValue())

        default: throw LangCoercionError.InvalidAddition
        }
    }
}

extension LangValue : Storeable {
    mutating func set(object object: LangValue, forKey key: String) throws {
        switch self {
        case let .TableValue(table):
            var copy = table
            copy[key] = object
            self = LangValue.TableValue(copy)

        default: LangCoercionError.NotOfTypeTable
        }
    }

    mutating func remove(forKey key: String) throws {
        switch self {
        case let .TableValue(table):
            var copy = table
            copy.removeValueForKey(key)
            self = LangValue.TableValue(copy)

        default: throw LangCoercionError.NotOfTypeTable
        }
    }

    func object(forKey key: String) throws -> LangValue? {
        switch self {
        case var .TableValue(table):
            return table[key]

        default: throw LangCoercionError.NotOfTypeTable
        }
    }

    func keys() throws -> [String] {
        switch self {
        case let .TableValue(table):
            return Array(table.keys)

        default: throw LangCoercionError.NotOfTypeTable
        }
    }
}

Conclusion

So that's it, that's the basic approach to the problem that I think is a concise, clear model of the specification. It doesn't exactly fit the asks from the four options, but I think it is the better approach.

Full source code here: https://gist.github.com/owensd/33d85872c15c2b496515

Brent’s Week 3 Bike Sheed