News: Stay up to date

The Étoilé community is an active group of developers, designers, testers and users. New work is being done every day. Visit often to find out what we've been up to.


Full Trait Support for Objective-C (or almost)

Posted on 12 July 2011 by Quentin Mathé

While working on the next EtoileFoundation release, I recently rewrote the Trait support, that David wrote in 2007 among various Objective-C improvements detailed in this report.

In the process, Mixin support has been removed in EtoileFoundation. For Objective-C, the class targeted by the super keyword is hardwired at compilation-time, in other words you cannot use super in a method that belongs to a mixin. As a result, mixins which heavily rely on super are pretty much useless. Mixins tend to heavily use super because every mixin application inserts a new implicit subclass. Hence multiple mixin applications on the same target class create a class hierarchy.

Finally a last issue with the Mixin support was that the new Objective-C runtime API had no class_removeIvar() and class_removeMethod() functions to play the tricks that make possible the implicit subclass creation.

As explained the Trait papers, Traits do the same than Mixins but in a cleaner and more predictable way.

Let's come back to the Trait support…

The idea behind traits is to share methods between unrelated classes. When inheritance isn't possible or is not the best design choice, traits enable reuse across classes. Each trait is a collection of methods. In that sense, a trait is similar to a protocol, but unlike a protocol it comes with an implementation.

To give a little background, the Objective-C trait support in EtoileFoundation is based on:

For Objective-C, David made the decision to implement Traits as Classes, rather than a separate construct as Squeak does. This leads to a very simple implementation, that requires no special Objective-C compiler or runtime support. It also means any class can be applied as a trait to another target class. The downside is that traits are applied to classes at run-time rather than compilation-time, so a class declaration doesn't list traits that apply to in it in a visible way, and we have to play hide-and-seek with the Objective-C type checking a bit.

It's worth to mention that GNUstep has provided a Trait-like ability named Behavior (see GSObjCAddClassBehavior()) that uses the same overriding rule, and Behaviors were introduced a long time before Traits were devised. Back in March 1995 :-) According to GNUstep Base ChangeLog, Andrew McCallum was the one that implemented the idea.

In EtoileFoundation, Trait support was previously minimalistic, limited to adding the methods that belongs to a class to another class. So an Objective-C Trait was roughly the same than a GNUstep Behavior. Now the interesting thing about Traits is the whole toolbox that comes with them. It's probably why GNUstep Behavior use has remained very limited. The motivation behind rewriting the Trait support was to support the whole toolbox:

  • trait operators (exclusion, aliasing)
  • composite trait (a trait with subtraits) and the flattening property that goes along

In our implementation where everything happens at run-time, trait applications are memorized to support composite traits and multiple trait applications to the same target class. Each time a trait is applied, it gets validated against the trait tree already bound to the target class. This ensures operators, overriding rule and flattening property will remain valid in the new trait tree. Unlike Squeak trait support, a trait can be applied at any time, and also the way the trait applications are memorized would make relatively trivial to unapply traits at runtime.

The basic API to apply a trait is

+[NSObject applyTraitFromClass:excludedMethodNames:aliasedMethodNames:]

where the receiver class is the class to which the trait is applied to. You usually would invoke this method in +initialize. To prevent, the compiler to warn you about the trait methods to be provided dynamically, you need to declare trait methods in a category on each target class or use a pragma to disable to protocol checking such as #pragma GCC diagnostic ignored "-Wprotocol" in case the trait corresponds to a protocol.

Here is a small example that applies two subtraits BasicTrait and ComplexTrait, to another trait CompositeTrait, then the resulting trait is applied to the receiver class.

NSDictionary *aliasedMethods = 
    [NSDictionary dictionaryWithObjectsAndKeys: @"lost:", @"wanderWhere:")];

[[CompositeTrait class] applyTraitFromClass: [BasicTrait class]
                        excludedMethodNames: [NSSet setWithObject: @"isOrdered"]
                         aliasedMethodNames: aliasedMethods];
[[CompositeTrait class] applyTraitFromClass: [ComplexTrait class]];

[[self class] applyTraitFromClass: [CompositeTrait class]];

aliasedMethods means -[BasicTrait wanderWhere:] is going to appear as -lost: in CompositeTrait.

In addition, it's possible to apply a trait without the overriding rule (that states target class overrides trait methods), which means methods in the target class can be replaced by methods from a trait. This is a bit closer to a mixin application and kinda similar to GSObjCAddClassOverride(), but its use should be restricted to clever hacks imo :-)

NSDictionary *aliasedMethods = 
    [NSDictionary dictionaryWithObjectsAndKeys: @"lost:", @"wanderWhere:"];

[[self class] applyTraitFromClass: [BasicTrait class]
              excludedMethodNames: [NSSet setWithObject: @"isOrdered"]
               aliasedMethodNames: aliasedMethods
                   allowsOverride: YES];

allowsOverride: YES means we allow the trait to override/replace methods in the target class.

Trait applications are commutative, so the ordering in which you apply traits doesn't matter… but when this mixin-style composition is used, traits are not commutative and the ordering matters. That's why I'd rather discourage its use.

Unlike in Squeak, you cannot send messages to super in trait methods (same problem than the one mentioned at the beginning about the Mixin support). It probably won't change in ObjC in the short term, because the possible solutions are heavy:

  • a new IMP() function that takes an extra argument that allow to evaluate the value of super when it's late-bound (Objective-C methods are compiled into C functions, and a IMP is a function pointer that can be used to access the C function related to a method)
  • trait method must be recompiled per target class

The last solution shouldn't be too hard to implement in Pragmatic Smalltalk (or rather LanguageKit). For declaring traits, it would interesting to extend our Smalltalk parser to support the Squeak syntax.

For now, three other limitations exist:

  • trait applications don't take in account class methods
  • no mechanism to declare and check non-trait methods required by trait methods (so you get a runtime exception instead)
  • traits must be stateless (no ivar access is allowed)

For the code and documentation, take a look at NSObject+Trait.