Pure functions are functions that don’t cause side effects. Their output is deterministic, meaning that given the same input, they will always return the same output. They also don’t access anything global. For example, if a pure function relied on a random number generator, the seed for that generator would have to be passed in as a parameter. With no external forces acting on them, these functions are easy to test and easy to reason about.
As I write more code, I find that the classes that I enjoy writing the most are those that resemble pure functions. I’ve taken to calling these classes “pure objects”.
What is a pure object? A pure object is initialized with several parameters, and responds only to a few readonly
messages. Ideally, it doesn’t access any global state, like singletons. If it does, it allows those to be overridden externally.
The interface for a pure object has three components.
- The inputs, which are passed to the initializer. These are the analog to the parameters of a pure function.
- Read-only access to the inputs. The purpose of this access is two-fold: first, they let you query the object to understand its inputs; second, they create instance variables to store those inputs. Crucially, these references can’t be changed once they are set in the initializer.
- The outputs, which are also represented as read-only properties.
Let’s take a look at an example interface:
@interface SKTimeAgoRenderer
- (instancetype)initWithDate:(NSDate *)date locale:(NSLocale *)locale;
@property (nonatomic, readonly) NSDate *date;
@property (nonatomic, readonly) NSLocale *locale;
@property (nonatomic, readonly) NSString *longTimeAgo; //"2 minutes ago"
@property (nonatomic, readonly) NSString *shortTimeAgo; //"2m"
@end
This specific example can be referred to as decoration. A date object is wrapped in a Renderer, which “decorates” it with extra behavior. Presenters and policies, which I’ve also written about on this blog, are other examples of decoration.
Used up and immediately discarded, pure objects are often short-lived:
[[[SKTimeAgoRenderer alloc] initWithDate:aDate locale:[NSLocale currentLocale]] shortTimeAgo];
The discerning reader might ask, why pass the date into the initializer? We could just as easily make a single TimeAgoRenderer and use it on multiple dates via a message like -shortTimeAgoForDate:
.
Passing the date to the initializer makes for a much simpler object. With the multiple-use TimeAgoRenderer, internal methods that needed no parameters before (like -dateInTheLastDay
, -dateWasYesterday
, and -dateInTheLastYear
) would all need the date to be passed to them, a certified code smell.
Further, with the single-use object, self.date
never changes, which reduces bugs and increases understandability. The only downside to discarding the object is allocation time, which, at close to 9 nanoseconds, is minimal. Initializing with the date yields simplicity and clarity with little penalty.
In what other ways is our pure object like a pure function? To return more than one result, a pure function takes advantage of a tuple, which is an ordered list of values. Not only can we return multiple values, our outputs are named and can be accessed in any order. (Python uses ”named tuples”, but most languages don’t have that feature yet.) Modern functional languages, such as Haskell, take advantage of lazy evaluation. In a similar way, our pure objects can be created in one part of the code, with their outputs uncalculated until they are required.
Pure functions also take advantage of currying. Currying a function lets you pass the initial parameters to a function to create a new function that only needs the final parameters. For example, we can make an addFive
by passing only one of the necessary parameters to the add
function:
add(5, 3); //8
addFive = add(5); //a curried function that will add 5 to its input
addFive(3); //8
We can do something similar with our pure objects, using convenience initializers. Assuming our normal use will probably pass [NSLocale currentLocale]
for the locale, we can create a convenience initializer:
- (instancetype)initWithDate:(NSDate *)date {
return [self initWithDate:date locale:[NSLocale currentLocale]];
}
This is slightly different than currying, since we have to define the curry ahead of time. It also has an advantage over currying, since we can “curry” the parameters in any order. The big benefit of defining convenience initializers is that we can inject our own parameters (such as the locale) when needed, such as during testing.
To make the usage even easier, we can wrap our entire object in a class method, like [SKTimeAgoRenderer shortTimeAgoForDate:date]
. We get the best of both worlds: a short incantation for the most common use case, as well as the ability for verbosity when the situation demands it.
Of course, side-effects are the whole reason we write code; we can’t only write these types of objects. Some of our objects must communicate with the network, write things to databases on disk, and render to the display buffer. Using small objects like these is a great technique to increase expressiveness, concentrating our side-effect-ful code and keeping our responsibilities separate.