Introducing ReflectableEnum

I'm happy to announce that I released ReflectableEnum – Reflection for enumerations in Objective-C last week. ReflectableEnum tries to fix some of the problems I've been having with enums in Objective-C for a long time. It's a small library, but I think it can find its place in many Objective-C codebases.

It allows you to access things not accessible when using the regular NS_ENUM macro:

  • get a string value for an enumeration's member (which is a common problem)
  • get all values used in an enumeration (also a prevalent issue; personally I find it really useful in unit testing)
  • get a minimum value in an enumeration
  • get a maximum value in an enumeration

You should definitely see it on GitHub.

Usage

To use it, you just have to replace existing enum definitions, like:

typedef NS_ENUM(NSUInteger, AccountType) {
  AccountTypeStandard,
  AccountTypeAdmin
};

with:

REFLECTABLE_ENUM(NSInteger, AccountType,
  AccountTypeStandard,
  AccountTypeAdmin
);

Now you can get a string representing an enumerator and minimum/maximum/all values of an enumeration the enumerator belongs to with:

AccountType accountType = AccountTypeStandard;
NSString *typeString = REFStringForMember(accountType);          // @"AccountTypeStandard"
NSInteger mininimum = REFMinForEnumWithMember(accountType);      // 0
NSInteger maximum = REFMaxForEnumWithMember(accountType);        // 1
NSArray *allValues = REFAllValuesForEnumWithMember(accountType); // @[@0, @1]

Implementation

REFLECTABLE_ENUM macro does two things:

  1. Creates a usual NS_ENUM definition.
  2. Creates a set of functions for accessing enum's data. The most important part here is that definitions of functions have a string containing __VA_ARGS__. With this little trick we're able to move the information that's normally available only in the source file to the runtime.

Here's what will be generated for the above (AccountType) enum (new lines in strings are omitted for ease of reading):

typedef NS_ENUM(NSInteger, AccountType) {
  AccountTypeStandard,
  AccountTypeAdmin
};

__attribute__((overloadable)) NSString *REFStringForMember(AccountType value)
{
  return private_REFString(@"AccountTypeStandard, AccountTypeAdmin", @(value));
}

__attribute__((overloadable)) NSInteger REFMinForEnumWithMember(AccountType _)
{
  return (NSInteger)private_REFMin(@"AccountTypeStandard, AccountTypeAdmin");
}

__attribute__((overloadable)) NSInteger REFMaxForEnumWithMember(AccountType _)
{
  return (NSInteger)private_REFMax(@"AccountTypeStandard, AccountTypeAdmin");
}

__attribute__((overloadable)) NSArray *REFAllValuesForEnumWithMember(AccountType _)
{
  return private_REFAllValues(@"AccountTypeStandard, AccountTypeAdmin");
}

__attribute__((overloadable)) NSString *REFStringForMemberInAccountType(AccountType value)
{
  return private_REFString(@"AccountTypeStandard, AccountTypeAdmin", @((AccountType)value));
}

NSInteger REFMinInAccountType(void)
{
  return (NSInteger)private_REFMin(@"AccountTypeStandard, AccountTypeAdmin");
}

NSInteger REFMaxInAccountType(void)
{
  return (NSInteger)private_REFMax(@"AccountTypeStandard, AccountTypeAdmin");
}

NSArray *REFAllValuesInAccountType(void)
{
  return private_REFAllValues(@"AccountTypeStandard, AccountTypeAdmin");
}

Functions from private_* family parse the string and return the values that we need. There's some caching going on too, but it's mostly a simple stuff.

Casting, i.e., what type an enumerator really is

When we try to pass an enumerator directly to one of the overloaded functions, like:

REFStringForMember(AccountTypeStandard);

we get a compile-time error:

Call to 'REFStringForMember' is ambiguous.

It happens because the compiler treats an enumerator as an NSUInteger and doesn't know which overloaded function to choose. I initially thought that this is something that could be improved in the compiler itself, because it looked to me like type inference is somewhat possible in this case. For example, if we have another enumeration called AnotherEnum, and we try to assign to a variable of its type a value from another enumeration, like:

AnotherEnum x = AccountTypeStandard;

we receive a compile-time warning:

Implicit conversion from enumeration type 'enum YourEnum' to different enumeration type 'AnotherEnum' (aka 'enum AnotherEnum').

So, I did what everyone what would do – asked Apple through a radar. The answer I received wasn't optimistic. It turns out that the compiler pretends that type information is associated with an enumerator only during diagnostic phase, just so it can generate more helpful warnings. This information is not preserved during actual compilation.

Other libraries

Michael Tsai pointed out that a library solving the similar problem already exists, JREnum by Jonathan 'Wolf' Rentzsch. I somehow missed it during my research. I really enjoyed comparing my implementation to Jonathan's.

Future

There's not much to be improved in ReflectableEnum. We probably shouldn't expect anything, as improvements for enumerations in Objective-C go, too. The next step is Swift. I won't be surprised if we see a reflection ability added to it sooner rather than later.