Brent Simmons posted a blog article today about a feature he was missing from Swift that was readily available in Objective-C – the ability to access properties of an object indirectly using a string.
Brent’s goal is to be able to write some generic code that compares various different properties. Here’s how he puts it:1
For every merge-able property there are really two properties, and there’s a simple naming convention.
For example, a note’s archived flag has two properties: archived and archivedModificationDate.
Merging works like this pseudo-code:
if existingObject.archivedModificationDate < serverObject.archivedModificationDate {
existingObject.archived = serverObject.archived
existingObject.archivedModificationDate = serverObject.archivedModificationDate
}But the code doesn’t actually look like that. Instead of duplicating the above for each property, there’s a single method that takes server object, existing object, and property name as parameters. Then it uses valueForKey: on both server object and existing object. (It gets the key for the date value by adding “ModificationDate” to the property name.)
This turns merging a given property into a one-liner:
mergeProperty(existingObject, serverObject, “archived”)
He asks if there’s a way to do this right now. The answer is almost, with some shortcomings (most of which will hopefully get resolved before the end of the beta).
Firstly, here’s an example version of a struct with some properties:
(I’m using Int
as a stand-in for a date/time object, for simplicity)
struct Properties { // someday soon these can be private var _name: String, _nameModificationDate: Int var _shape: String, _shapeModificationDate: Int init(name: String, nameModificationDate: Int, shape: String, shapeModificationDate: Int) { self._name = name self._nameModificationDate = nameModificationDate self._shape = shape self._shapeModificationDate = shapeModificationDate } func name()->String { return _name } func nameModificationDate()->Int { return _nameModificationDate } func shape()->String { return _shape } func shapeModificationDate()->Int { return _shapeModificationDate } }
Note that the “getter” methods for name and shape are methods, not var properties with a { get }
. This will be important later.
This struct is both immutable in the sense that there are no setter methods, and also can be immutable because it’s a struct you can declare with let
instead of var
. There’s an important distinction here – classes can be immutable too, in the former sense. A pattern in the Swift standard library is to declare an immutable version of a protocol that only has read methods (such as Collection
, that declares an rvalue version of subscript
), and then an inherited version that is mutable and has write methods (such as MutableCollection
, that declares an lvalue version of subscript
).
Of course, right now there’s no private variables in Swift, so internal variables are still writeable, but Apple have made it pretty clear this is coming in a later beta.
Having declared our struct, we create a dictionary that can be used to access the getter methods:
let propertyDict = [ "Name": (Properties.name, Properties.nameModificationDate), "Shape": (Properties.shape, Properties.shapeModificationDate), ]
This dictionary maps a string name onto a tuple containing two member function references,2 one for the property and one for its associated modification date. They have a type of Properties-
>()-
>String
(or -
>Int
). You use them first by applying an instance of the object, which returns a new function bound to that instance, and then calling the function.
Here is why the accessors had to be methods. To my knowledge, you can’t do the same thing with var propertyName { get }
properties, for the simple reason they don’t need ()
to invoke, so you can’t leave it off to get the method reference. Maybe under the hood there’s a secret get_propertyName()
function you could use, but I can’t find it.
To demonstrate these in use, here’s the implementation of the mergeProperty
function:
func mergeProperty(existingObject: Properties, serverObject: Properties, propertyName: String) -> (String, Int) { // fetch the property-accessing member function references // associated with propertyName if let (property, propModDate) = propertyDict[propertyName] { // now get the modification date for this property // for both the existing and server versions let existingModDate = propModDate(existingObject)() let serverModDate = propModDate(serverObject)() // do your date comparison to pick the winner if existingModDate >= serverModDate { return (property(existingObject)(), existingModDate) } else { return (property(serverObject)(), serverModDate) } } else { // throw an exception! (only kidding, in practice mergeProperty's // return type should probably be optional and return nil to // account for this possibility return ("",0) } }
And here is the mergeProperty
function in use:
let existingObj = Properties(name: "Fred", nameModificationDate: 10, shape: "Square", shapeModificationDate: 20) let serverObj = Properties(name: "Bob", nameModificationDate: 12, shape: "Circle", shapeModificationDate: 18) let (mergedName, _) = mergeProperty(existingObj, serverObj, "Name") let (mergedShape, _) = mergeProperty(existingObj, serverObj, "Shape") // mergedName = Bob, and mergedShape = Square
There are a lot of likely objections to the above code, some of which can be addressed by more code, some by possibly-upcoming Swift enhancements, and some that are just fundamental to Swift.
The hardcoding of the entries in propertyDict
. This is the inevitable result of there being no official reflection in Swift. Yet. There’s clearly the beginnings of reflection with the getMirror()
method and AnyClass
type. I’d be surprised if Swift made it all the way through beta without it being added. Once there’s reflection, you could build up propertyDict
at runtime (and also hopefully compile time if you prefer) based on a list of property names and a naming convention.
Jason Peebles tweeted an alternative, that involves access to the raw member variables using the nascent reflection capabilities. This is very interesting, and kind of like solving this from the other end. My concerns with this approach is it feels better to me to use methods, though maybe that’s my own prejudice. Also, that current reflection API is possibly temporary and only there to facilitate the playground and debugger. Eventually when full reflection is implemented, hopefully both methods and member variables will be exposed (maybe they are already with a bit more playground spelunking).
It only supports strings. In real-world use, you would wrap up propertyDict
and mergeProperty
together into a class (along with the reflection that took the metaclass and a list of property names), which you could then make generic to operate on Int
or String
accessors depending on need.
Use of methods instead of properties. I don’t know how to fix this right now. Leave a comment if you do! But for the most part, anything you can do with properties you can also do with getting and setting methods, so it’s not a deal-breaker.3 This is the biggest sticking point though.
It’s not how it works in Objective-C. More of a philosophical difference, this one. But you could probably reproduce nearly all the behaviour of valueForKey
using the above technique. The solution using generics is still going to leave you fighting the type system a bit more than you might in Objective-C, and while using Any
instead might get you closer, I usually find myself fighting even harder with Any
than I did with types.
An implementation that is true to the valueForKey
behaviour, by which any property can be fetched with a single method call, seems by definition to require a return of an Any variable which gives up much of the type safety Swift is forcing you towards. Maybe whatever reflection solution eventually emerges will show a pleasing middle-ground.
Anyway, hopefully this solves at least part of Brent’s problem. Maybe some day some of this code will make it into Vesper, which would be very exciting as I’m a big fan of the app.
- Sorry about butchering the code part. My blog engine and I are not getting along. ↩
- I made that term for them up, by the way. It’s not like an official name for them or anything. It’s what they’re called in C++, which Swift seems to model in this respect. ↩
- At least until Apple implements KVO in Swift only for properties. Then this technique is screwed. ↩
According to your code,
““
if existingModDate >= serverModDate {
return (property(existingObject)(), existingModDate)
}
else {
return (property(serverObject)(), serverModDate)
}
““
why the output would be “// mergedName = Bob, and mergedShape = Circle”?
I think it should be “Bob & Square”
And, indeed, in my playground where I tested and cut and pasted all but that comment, that’s exactly what it is. That’ll teach me to type stuff by hand. Fixed, thanks for the spot. Hey at least it was a bug in my blog, not a bug in my code!
One more question…
as “func name()->String { return _name }”, how can propModDate(existingObject)() work?
e.g
propModDate would return func nameModificationDate()->Int { return _nameModificationDate }
so…what does “existingObject” in “propModDate(existingObject)” means?
propModDate(existingObject)() means take propModDate, which is a class member function, and bind it to a specific instance, existingObject, returning a new function which, when called, is equivalent to calling existingObject.propModDate(). The magic is that propModDate might either refer to nameModificationDate or shapeModificationDate, depending on which one you pulled out of the dictionary.
Well, as to the current code for example, take the propertyName “Shape”
propModDate means the function Properties.shapeModificationDate, also means func shapeModificationDate()->Int { return _shapeModificationDate }
My question is if i call Properties.shapeModificationDate(existingObject), swift would automatically translate to existingObject. shapeModificationDate right?
I test this in my playground, and it will work.
Btw, where does this feature Apple declared…I have not found it before.
Good article and thanks for the mention. I agree my code snippet feels a little, er, “undocumented”. I wrote that snippet while at WWDC (and it crashed XCode 6 Beta 1’s Playground) and spoke to the Swift team about it while there. It sounds like the “Mirror” structures are there for debugging and Playground purposes for now, although who knows what it will represent by the time version 1 is out. They’ve already redesigned Array, after all.