Monday, January 21, 2008

Oooh, NSPredicate functions...

I was looking for a way to manage a set of unique managed objects. Essentially, I needed a heterogenous collection of values whose uniqueness is entirely denoted by their value, without naming or indexing properties. This is set semantics of course, and ordinarily one could rely on -hash and -isEquals defined properly on the objects to ensure their uniqueness in the set. However, with NSManagedObject, you are not permitted to override these methods (they are presumably used to define uniqueness by id in the managed object context). Nothing stops you from defining your own version of isEquals in your subclass however, and this can compare the single attribute payload of one of these objects, with another (using isEquals on the payload objects).

Having achieved that, one can then expect to be able to use -filteredSetUsingPredicate on the to-many relationship (manifesting as an NSMutableSet) defined in the Core Data model for the set of unique objects. However, how does one effect a call to the special equality method in the content objects?

While there's not a whole lot of documentation and examples on the subject (and actually the NSPredicate class reference AND guide are entirely silent on the matter), the NSExpression class reference mentions "Function Expressions", which I stumbled upon while investigating NSExpressions and whether you could use them as a mini expression language aside from their utility in defining predicates.

The documentation for Function Expressions seems to consist of 7 sentences and a single trivial example. However, this was enough to spike my curiosity.

The basic syntax is described as:
FUNCTION(receiver, selectorName, arguments, ...)
...and the target selector is described as having to accept zero or more id arguments and returning an id value.

Thus, my custom isEqualsPayload method could receive its test object as normal, though it would have to deviate from a normal 'isEquals' by returning a BOOL NSNumber instead of the unboxed primitive BOOL.
In the FUNCTION syntax itself (passed to the NSPredicate factory method), my target object is clearly SELF, and the argument object can be passed in the regular format style as a simple %@. I screwed up the selector a few times (too much haste, less speed, and following the simplistic example I forgot to put a colon on the selector name, as my example passes one argument). Several crashes later (yes, NSPredicate seems a little bit keen to die badly if you get this syntax wrong - but I guess that's the price to pay for both dynamic invocation AND a variadic signature) and I had my custom equality method being called - and a nice unique set/relationship. There IS a caveat attached to FUNCTION in the context of Core Data. The reference says (one of the seven sentences!): "Note that although Core Data supports evaluation of the predefined functions, it does not support the evaluation of custom predicate functions in the persistent stores (during a fetch).". I'm hoping that my case does not intersect with the unsupported case (qualified by "during a fetch"), though I'll need to test it with the SQLite persistent store to see if faulting causes a problem. Hopefully however, faulting will occur independently of the execution of this kind of predicate (on the relationship NSMutableSet), and all filtering will occur AFTER the objects have been fully fetched into the set. If not, my implementation (with a relatively small number of objects intended in these sets) might be able to live with a forced fetching of all members of the relationship.

On the subject of uniqueness in Core Data, it would be rather nice if this could be declared somehow. Gating the addition of objects with checks for uniqueness is awkward, though admittedly the semantics of uniqueness could be more complicated than a local (single attribute) test (e.g. via tuples/joins). It's interesting that Core Data does have an 'index' flag on attributes, which got my excited at one point, thinking that maybe this denoted a 'unique key' metadata on the attribute. However, the documentation simply states that this flag is for marking "attributes that might be useful for searching", which indicates a rather less formal meaning.

1 comment:

Anonymous said...

Can you post an example of the syntax, I can't make it work. :(