Using custom functions with NSExpression

There are a bunch of built-in functions in the NSExpression class that allow you to do some pretty neat stuff. They are: average:, sum:, count:, min:, max:, median:, mode:, stddev:, add:to:, from:subtract:, multiply:by:, divide:by:, modulus:by:, sqrt:, log:, ln:, raise:toPower:, exp:, ceiling:, abs:, trunc:, random, random:, now, floor:, uppercase:, lowercase:, bitwiseAnd:with:, bitwiseOr:with:, bitwiseXor:with:, leftshift:by:, rightshift:by:, onesComplement:, and noindex:. That’s a lot of functions! You have your arithmetic functions, some multi-parameter functions, some bitwise functions, and a few miscellaneous ones thrown in for good measure.

But… what if you want to find the factorial of a number? Or use any sort of trigonometric function? Or say you wanted to do something much more complex? Are you out of luck?

Fortunately, no. There is a way to do custom functions in NSExpression, with the downside that the syntax is a little bit more complex than your standard functions.

The built-in functions can be written as if they were actual functions: now(), sqrt(42), 6 * 9, etc. Unfortunately, we cannot do that with our custom functions. Oh well. Let’s get over that and move on.

The Syntax

The syntax for all functions is:

FUNCTION(operand, 'function', arguments, ...)

In a nutshell, the result of the function is the result of invoking the function method on operand, while passing in the arguments as parameters to the method.

If we dive into one of the NSExpressions produced by a standard function, we learn some interesting things. Let’s example the function “6*9”.

NSExpression * e = [(NSComparisonPredicate *)[NSPredicate predicateWithFormat:@"6*9 = 1"] leftExpression];

The -operand of this expression is another NSExpression. In the case of these built-in functions, it turns out that it’s a constant value expression encapsulating a Class object of type _NSPredicateUtilities. Some runtime introspection on this object shows that it has a whole bunch of class methods that correspond to all of the built-in functions, along with one that’s not listed in the documentation: castObject:toType:. We previously discussed using this function with advanced date predicates.

The operand is an NSExpression (as opposed to an id), because the object receiving the method could be the result of another expression.

The function in our multiplication expression is “multiply:by:”, as we would expect.

The arguments to our multiplication is an NSArray of NSExpression objects. They are expressions for the same reason that the operand is an NSExpression. In our example, we have two expressions, both representing NSNumbers as constant values.

Custom Functions

Now that we understand the syntax behind FUNCTION(), it should be fairly obvious how to go about making our own functions. Here’s what we need:

  • An object on which to operate.
  • A method to invoke
  • Parameters to pass in (optional)

So let’s say we want to add a factorial function. We have two options on how to approach this:

  1. Make our factorial method receive the number as a parameter
  2. Make our factorial method operate on a number directly

I personally prefer the latter approach, since the syntax is slightly less complicated. Here’s what we would do:

@interface NSNumber (FactorialExpression)

- (NSNumber *) factorial;

@end

@implementation NSNumber (FactorialExpression)

- (NSNumber *) factorial {
  double baseValue = [self doubleValue];
  double result = tgamma(baseValue+1);
  return [NSNumber numberWithDouble:result];
}

@end

(This method uses the Gamma Function, which allows for computing the factorial of non-integral numbers)

Then in our format string, we would express it like this:

FUNCTION(4.2, 'factorial')

If we evaluate that expression, we get 32.57809605033135, as we were hoping! The key thing here is realizing that 4.2 will be boxed in an NSNumber, which means that the factorial method will be invoked on NSNumber.

Venturing Toward the Absurd

OK, we’ve got basic functions down. What about functions that take arguments? For this next example, we’ll take a look at a custom function being used in StackKit, a framework for accessing the Stack Overflow API.

At the heart of StackKit is a URL generation system that will turn a combination of an NSPredicate, an NSSortDescriptor, and a Class into an HTTP GET request that can be used to retrieve data from one of the Stack Exchange sites (like http://stackoverflow.com). Due to the limited nature of the API, only certain predicates and sort descriptors are allowed, and only in very specific combinations.

One of the parts of “validating” an NSPredicate is to verify that it is only using allowed keyPaths, and that these keyPaths are being compared with specific operators. Each of the “RequestBuilder” classes vends an NSDictionary called the recognizedPredicateKeyPaths. This dictionary is a map of keyPaths (as the key) to an NSArray of NSNumber objects. The NSNumbers box an NSPredicateOperatorType.

Since each builder recognizes different keyPaths and different operators, I had to get a little creative. Here’s what the code looks like:

p = [NSPredicate predicateWithFormat:@"FUNCTION(%@, 'sk_matchesRecognizedKeyPathsAndOperators:', SELF.recognizedPredicateKeyPaths) == YES", predicateToValidate];
[builders filterUsingPredicate:p];

In this case, predicateToValidate is the user-supplied NSPredicate. I’m going to be passing this predicate in as the operand to my FUNCTION. The method that will be invoked on it is @selector(sk_matchesRecognizedKeyPathsAndOperators:), which is conveniently provided via an NSPredicate category. Finally, the argument to this function is the dictionary of the current request builder. If the sk_matchesRecognizedKeyPathsAndOperators: method returns YES, then we’ve passed this particular step of the validation.

The code for the NSPredicate method is fairly simple; it simply iterates through all of the keys in the NSDictionary, then extracts the subpredicates of the “predicateToValidate" for each key. Of these subpredicates, if there’s one that’s using an operator that’s not recognized, then we abort and return NO. Otherwise we’ll get to the end, find that all of our subpredicates are using allowed operators, and return YES.

The result of those two lines of code is that the builders array will be filtered to only contain the builders that allow the left keyPaths and operators that are specified in the user-provided predicate.

(For more fun, exciting, and absurd uses of NSPredicate, check out SKRequestBuilder.m.)

A Caution

One of the things to be wary of when using custom functions is that all of the parameters to the method must be objects, and the return value of the method must also be an object! This means that you cannot do:

[NSPredicate predicateWithFormat:@"FUNCTION('a', 'isEqual:', 'a') == YES"];

If you try to evaluate that predicate, it will crash. NSExpression is attempting to cast the result of the isEqual: method as an object. However, (id)1 is not an object recognizable by Objective-C, no matter how you cast it.

Instead, if you needed to do this, you’d add a method that simply boxed the result in the appropriate NSValue object and use that method:

@implementation NSString (CustomFunction)

- (NSNumber *) dd_isEqual:(id)other {
  return [NSNumber numberWithBool:[self isEqual:other]];
}

@end

...

FUNCTION('a', 'dd_isEqual:', 'a')

This will work just fine.

Wrap Up

Custom functions in NSExpression are pretty neat. You don’t find yourself using them very often, but when you need them, they can be extremely handy!

  1. wickedfireisgreat reblogged this from funwithobjc
  2. funwithobjc posted this
Short URL for this post: http://tmblr.co/Zt522y2kBaL8
blog comments powered by Disqus