Dustin Bachrach

mobile engineer / developer platform enthusiast / dog lover

Sequence Unpacking in Objective-C

I like to look at other programming languages for inspiration. Most of us work in a single language on a daily basis. We start aligning our thought process to that language. We settle into certain tropes and styles based on the technical limitations of our language. Exploring other languages can introduce us to new concepts— like the simple conveniences in Ruby’s syntactic sugars, or the complex metaprogramming available in C++’s templates. In this post, we’ll take a look at one of Python’s features known as sequence unpacking.

Sequence unpacking

Python has this little beauty of code to swap variables a and b:

1
a,b = b,a

This is Python’s sequence unpacking. It’s obvious, concise, and extensible. Sequence unpacking goes by several names in other languages— ‘destructuring bind’ in Lisp or ‘parallel assingment’ in Ruby. We’re going to dive deeper into this feature with an Objective-C hat on. There are three typical uses for sequence unpacking:

  • Easy swapping is simple, elegant, and rids us of a temporary variable cluttering up things. Here’s swap without sequence unpacking:
1
2
3
4
5
int a = 5;
int b = 6;
int temp = a;
a = b;
b = temp;
  • Multiple return values ease the calee, so it can return distinct values without having to generate a new type or pass things by reference through extra parameters. Consider how you delete files:
1
2
NSError* error;
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:path error:&error];

The remove method communicates two things— whether the file was removed, and detailed error information. With sequence unpacking you could do something similar:

1
2
3
BOOL success;
NSError* error;
(success, error) = [[NSFileManager defaultManager] removeItemAtPath:path];
  • Parsing out complex types eases the caller, so it can work directly on the needed data. Imagine you want to find the area of a UIView. You have to grab the bound’s size and then query for its width and height:
1
2
3
4
CGSize size = view.bounds.size;
CGFloat width = size.width;
CGFloat height = size.height;
CGFloat area = width * height;

With sequence unpacking you could move to a more concise version:

1
2
3
CGFloat width, height;
(width, height) = view.bounds.size;
CGFloat area = width * height;

The problem sequence unpacking solves is not huge. We make due in all three of these use cases with Objective-C. What sequence unpacking does is let us express our true intent succintly.

We all realize that when swapping two variables, we need to copy one value temporarily. Does that mean we need to explicitly write that? We know that you can simulate multiple return values with out parameters. But should we be forced to hide our intent through inderection? When we recieve a composite type, sure we can store it away and separately access its members. But why can’t we just directly recieve what we want in the format we want?

Let’s explore adding sequence unpacking to Objective-C. The source code to this blog post is available on GitHub under the MIT License.

Implementation

I’ll first declare the ideal syntax for sequence unpacking in Objective-C:

1
2
int x, y;
(x, y) = GetPoint();

Fat chance of that. Here’s the best we can do:

1
2
int x, y;
unpack (_(x, y) from GetPoint());

This syntax makes it look like we’ve introduced two new keywords— unpack and from. In reality from is just #defined from ,, so we’re left with:

1
unpack(_(x,y), GetPoint());

unpack is a function that takes something to pack up and somewhere to unpack it.

1
2
3
4
void unpack(id<PAKSeqUnpacker> unpacker, id<PAKSeqPacker> packer)
{
    [unpacker unpack:[packer pack]];
}

Two protocols define what’s required to make something packable or unpackable.

1
2
3
4
5
6
7
8
9
10
11
@protocol PAKSeqPacker <NSObject>

- (NSEnumerator*)pack;

@end

@protocol PAKSeqUnpacker <NSObject>

- (void)unpack:(NSEnumerator*)enumerator;

@end

If you want your class to be packable, then implement pack and return an enumerator. To allow unpacking, just implement unpack:, and you’ll recieve the enumerator that the packer created.

With these definitions, we have a flexible sequence unpacking system. The protocols let us easily support both packing and unpacking to/from arbitrary classes. With enumerators, classes can pack into any number of slots. Finally, the unpacker can unpack a different number of slots than the packer packed. For example, unpack (_(x,y) from Get3DPoint()) would only unpack the x and y coordinates of a three dimensional point and ignore the z coordinate.

Passthrough Tuples

You probably are wondering what this _(...) thing is. _ is a macro for easily creating a Tuple. What’s a Tuple? It’s a fixed and ordered list of elements. It’s a lightweight array. Having the _ macro for a Tuple makes it nice for packing up objects without having to create a custom class to contain them since Tuple implements the PAKSeqPacker protocol.

1
unpack (/* to some place */ from _(@1, @2));

Here, we’ve packed up 1 and 2 easily by placing them into a Tuple, and letting the Tuple take care of packing them.

It’s more interesting when we want to easily unpack values:

1
2
int x, y;
unpack (_(x, y) from /* some where */);

When the _ macro Tuple is on the left of the from, it acts as a simple container for unpacking variables. In the exmaple above, we want to unpack a value into the x variable. The Tuple needs to store a pointer to x. Passthrough Tuples store pointers to their contained objects. This allows them to read the contained variables’ values if the Passthrough Tuple will be packed, or write to the variables if the Passthrough Tuple will be unpacked.

So not only is _ acting as a shorthand for creating Passthrough Tuples, it’s also taking its parameters by reference instead of by value. It does this like so:

1
#define _(a, b) [PAKPassthroughTuple tupleWithA:(__strong id* )&a B:(__strong id* )&b]

Variadic Tuples

The definition of _ above is limited to only two parameters. Ideally, PassthroughTuple should take any number of parameters. The solution for this is not pretty.

1
2
3
4
5
6
#define PT_1(a) [PAKPassthroughTuple tupleWithValues:(__strong id* )&a, nil]
#define PT_2(a, b) [PAKPassthroughTuple tupleWithValues:(__strong id* )&a, (__strong id* )&b, nil]
// ... and so on to PT_10

#define GET_PT_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, NAME, ...) NAME
#define _(...) GET_PT_MACRO(__VA_ARGS__, PT_10, PT_9, PT_8, PT_7, PT_6, PT_5, PT_4, PT_3, PT_2, PT_1)(__VA_ARGS__)

We create a macro for creating a PassthroughTuple with one argument and call it PT_1. We make another one for two arguments and call it PT_2. We do this all the way up to PT_10.

Given these 10 macros, we then need a way to define an _ macro which can, based on the number of arguments, call the appropriate PT macro. We can do this by using this excellent trick.

Foundation support

Many Foundation classes can trivially be extended to support sequence unpacking.

  • NSEnumerator
  • NSArray
  • NSOrderedSet
  • NSDictionary

For example, here’s how we make NSArray packable:

1
2
3
4
5
6
@implementation NSArray (OCPAK)
- (NSEnumerator*)pack
{
    return [self objectEnumerator];
}
@end

With Foundation support, we can now do:

1
2
3
id a, b, c, k1, k2;
unpack (_(a, b, c) from @[@1, @2, @3]);
unpack (_(k1, k2) from @{@"a key" : @"value1", @"another key" : @"value2"}); // note: dictionary order is not guranteed

Packing structs

This sequence unpacking implementation only supports objects. It can’t pack primitives like structs. This is unfortunate since one of the examples I gave was to unpack a CGSize.

The only way to do this is to wrap the struct in an NSValue, and make the values you unpack into id (unpacked as NSNumbers).

1
2
3
CGSize size = CGSizeMake(20, 30);
id width, height;
unpack (_(width, height) from [NSValue valueWithCGSize:size]);

This requires us to add packing support to NSValue:

1
2
3
4
5
6
7
8
9
10
11
@implementation NSValue (OCPAK)
- (NSEnumerator*)pack
{
    if (strcmp(self.objCType, @encode(CGSize)) == 0)
    {
        CGSize size = self.CGSizeValue;
        return [@[@(size.width), @(size.height)] objectEnumerator];
    }
    return nil;
}
@end

Even cleaner in C++

Turning an eye towards another language, C++, it should be entirely possible to duplicate this work in C++. What could be even more interesting is to take advantage of C++’s operator overloading. In C++, classes can define implementations for operators like +, , etc. With that tool, I think you can make sequence unpacking as simple as:

1
_(x, y) << GetPoint();

If you are comfortable giving up the = operator to sequence unpacking, then you could even do:

1
_(x, y) = GetPoint();

That’s pretty amazing.

Final thoughts

Sequence unpacking is a convenient feature in other languages, and I wanted to see how we could bring that to Objective-C in the most concise way possible. The solution is flexible and functional, but has its limits. To make it look integrated into the language, I defined from to be a ,, which isn’t ideal. This solution also struggles with primitives, having to wrap them in NSValues. However, it’s really interesting to see how these sorts of features can be brought to an existing language. I hope you enjoyed this exercise, and feel free to get in touch if you have any thoughts.