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>
.

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:
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