Skip to content

Instantly share code, notes, and snippets.

@vinhnx
Last active August 29, 2015 13:57
Show Gist options
  • Save vinhnx/9499702 to your computer and use it in GitHub Desktop.
Save vinhnx/9499702 to your computer and use it in GitHub Desktop.
Postmodern Programming

by Rob Rix | [original source](https://github.com/robrix/Postmodern-Programming/blob/master/Postmodern Programming.md) | better format

Foreword: What’s In a Name?

The schedule calls this talk “Building a Better Mousetrap: Declarative Programming and You.” Mark Dalrymple calls it the same, but with “Moose” instead of “Mouse.” I’ve tried on a few other names for it in the meantime, and am now thinking of it as Postmodern Programming. I hope you’ll forgive the change, but I think this new title more accurately describes its subject.

I’ve gone through many iterations of this material, including a half-written GitHub client which was to be used to inspect its own source code and commits during the talk. I call it Navel-Gazing. We won’t be looking at it today (although it’s on my github page if you’re interested), but I think you’ll find its name appropriate to the talk, too.

As to why “Postmodern Programming” or “Navel-Gazing” or any of this might fit, I will tell you a fact about me that has never been and will never be listed on my résumé: I can type at sustained speeds of around 140wpm. I spend all day in front of a computer and much of it hammering away on a keyboard with a sound like rain falling steadily on a corrugated iron roof, but I don’t list that fact on my résumé because it’s completely irrelevant: nobody pays a programmer for their typing skills.

The important parts of the job are done with our minds. It’s how we think about things, and what things we think up, and how we arrange these things. This is a talk about programming in the abstract; it’s a talk about abstraction in the abstract. But we shouldn’t take that too seriously.

Really, this is a talk about language and languages, but while it features Objective-C but isn’t particularly about Objective-C. Nothing I talk about today is going to make you a better Cocoa programmer, but it might make you a better programmer—that is to say, you will very likely make you a better programmer, and I will be very glad indeed if I have helped in some small way.

I tell a (small) lie: it is a little bit about Cocoa, in that it is about declarative programming and how to do it, and if we’re using Cocoa, which is rather the point of this conference, then obviously it’s going to intersect with that somewhat. But on the whole these things apply rather more widely, and it can be a good thing to think of oneself as a programmer, rather than as a Cocoa or Cocoa Touch programmer.

Of course, being that this is a talk about declarative programming, it might be useful to discuss what that even means. Wikipedia (the authoritative repository of all human knowledge) gives us three definitions:

  1. A program that describes what computation should be performed and not how to compute it
  2. Any programming language that lacks side effects (or more specifically, is referentially transparent)
  3. A language with a clear correspondence to mathematical logic

Well, that certainly clarifies things. And as an aside: you have to love any concept whose wikipedia page includes the heading “Subparadigms.”

For the moment let’s think of it as “not imperative programming,” where “imperative programming” is you, the Grand Imperator, telling the program what to do. Declarative programming would instead make you the Grand Declarator: you instead tell the program what it is. And, with that, the program no doubt walks away enlightened. (Even if we don’t.)

Prologue: Everything I Know Is Wrong

My name is Rob Rix. I’ve been a programmer for as long as I can remember, I’ve got twelve years of professional experience behind me, and I’ve spent the last eighteen months doing consulting and product development with Black Pixel. Before that, I spent eighteen months implementing sync, and so I know a thing or two about making mistakes.

All of which is to say: I know what I’m talking about, or at least, I think I do. But you shouldn’t take this as fact. Nothing in this talk is a stroke of genius or a flash of inspiration. Nothing in this talk is particularly new. If it’s new to you, there’s nothing wrong with that, either—it just means it’s your lucky day, and for that matter, mine too. So please don’t take my word on anything: if there’s anything in here that strikes you as interesting or dull or likely or implausible, try it out for yourself. See where it takes you. More than anything else, I would love to see you make things.

In the meantime, some things in this talk are bad ideas. Some of this is bad advice! Certainly if Monday morning you resolve to do nothing but what I talk about today then you are going to be very sorry when you get fired for it.

Nevertheless, that which I think I know, follows: this is a talk about making mistakes.

Part the First: Don’t Repeat Don’t Repeating Yourself

Every day, we write code we’ve written before. We all do this, all the time. This isn’t us practicing, either; it’s boilerplate. “Boilerplate” ought to be a swear word.

Apple has done a lot to reduce this which we often take advantage of because we’re programmers and therefore we hate doing things twice when we could have done them once. So we use properties (no matter what our feelings on dot notation are), since that means we can write less boilerplate accessor code. And we’re a little happier for doing so. This is good for us!

Because any time we write the same code twice, we are wasting ourselves. By definition, it could have been abstracted. We could have written it once. Perhaps the language makes it unclear, or difficult. Often we’re writing what is only mostly the same code, but there are tangled-up bits of differences swimming about in the middle of it. Perhaps it’s so straightforward that pulling the code we’re repeating out into another method and using that instead is going to result in a net increase in the amount of code we write. Why make things more complex than you have to?

There is no silver bullet. A professional must be prepared for the fact that she will have to make compromises, to do things the “wrong” way, in the course of her duties, and that every action she takes is a trade-off. But neither does she forget that she has repeated herself, or what the trade-off she made was, or why she made it.

There is, therefore, a time to write the code twice. But if you always do that, you’re never going to get anything done. We cannot rebuild the universe every time we want a piece of pie!

And unfortunately, even if we were to simply avoid repeating ourselves, it wouldn’t be enough.

Part the Second: Abstraction, Abstracted

It’s famously said that there are only two hard problems in computer science: cache invalidation, naming things, and off-by-one errors. (This is a talk about the median value: about naming things.)

Until recently, I thought that joke was saying that picking the name is the hard part: “Is this a ManagerFactoryObjectController or a ControllerFactoryManagerObject?”

That’s a joke, but perhaps not the joke. In truth, it isn’t easy selecting a name, but that’s not the hard part by half. The hard part is selecting something which is deserving of a name: abstracting.

How many of us have implemented view controller containment in a UIViewController subclass? Perhaps this is a view controller which decorates some child view controller with some common widget or branding that we want to use in various parts of our app?

You have to keep track of a lot of things: calling the correct containment methods on the old one in the right order, likely assigning it to a property, removing the old subview, calling the correct containment methods on the new one in the right order, adding the new subview, ensuring it has the right constraints or frame…

If you do all of this ad hoc any time you need to set the child view controller, it quickly becomes an intractable mess strewn across the -viewDidLoad, -viewWillAppear:, -viewDidAppear: and other methods of every view controller we might want to apply this treatment to. Instead, we abstract that into a method:

-(void)setChild:(UIViewController *)child {
    [_child willMoveToParentViewController:nil];
    [_child.view removeFromSuperview];
    [_child removeFromParentViewController];
    _child = child;
    [self addChildViewController:child];
    [self.view addSubview:child.view];
    child.view.frame = self.view.bounds;
    [child didMoveToParentViewController:self];
}

Is this an abstraction? Sure. It abstracts the act of setting the child. Does what it says on the tin!

Even if we’re only writing it once, that’s a lot of code to have to write in order to do a single thing. Why didn’t Apple make this more convenient? Why do you have to call willMove/remove and add/didMove paired like that? Why do you have to add and remove the subview yourself?

What if you want to animate the transition? They can’t just add and remove the subview on your behalf. They also can’t necessarily just call remove or didMove on your behalf, since those should be called when the animation is complete. You can use -transitionFromViewController:… to handle much of this, but it takes two view controllers which are already children, so some of this is still going to have to be handled externally.

This is unwieldy, but it seems that there’s no good way around it being unwieldy. That’s ok; it’s not like it’s costing us anything, unless we need to write something slightly different, in which case we can copy and paste this and tweak it to do what it needs to do in the new context and get over it. Move on.

We know about this sort of thing. Sure, we might repeat ourselves a little, but we have more important things to worry about. And code is never going to be perfect, anyway; that’s idealistic nonsense bordering on sociopathy.

But…

Before iOS 7/OS X 10.9, how many of wrote a -firstObject method in a category on NSArray? What did it look like?

-(id)firstObject {
	return self.count? self[0] : nil;
}

This is abstraction too, isn’t it? We’ve named something.

Perhaps we wrote this method after getting fed up of writing this out by hand every time we wanted to reason about the first object in an array. We might have been doing this directly, explicitly using the logic long-form:

-(NSString *)firstName {
	NSArray *nameComponents = [self.fullName componentsSeparatedByString:@" "];
	return nameComponents.count? nameComponents[0] : nil;
}

We might also have been doing it indirectly:

-(void)cycleSubviews {
	if (self.view.subviews.count) {
		[self.view sendSubviewToBack:self.view.subviews[0]];
		[self.view setNeedsLayout];
	}
}

In both cases we’re employing the concept of -firstObject even if we haven’t yet named it. Is -firstObject an abstraction? Absolutely. And it’s satisfying: it does exactly what you want. You’re not going to have to change it; there’s nothing to tweak. It is as close to perfect as anything we write can be.

What makes this different from -setChild:? Is it the parameter? Is it just the length? Is there a shorter example that shows the same contrast with -firstObject? Maybe you have a flower shop app and are writing a view controller to enter the destination details:

-(void)setUpNavigationItem {
	self.navigationItem.title = @"Recipient";
	self.navigationItem.prompt = @"Where would you like the flowers sent?";
}

This isn’t unwieldy at all: there’s only two lines of code in this method. It’s trivial. In fact, why would you even write this method? Why would anyone ever write this method, except to make -viewDidLoad maybe a couple of lines shorter? It’s ridiculous: we are going to call this all of once, and you’ve actually increased the program’s length by a net three lines of code: one for the declaration of the method, one for the closing brace, and one to call it in -viewDidLoad.

Furthermore, if you subclass it, and you don’t know that this method exists, you’re going to have to make sure you’re calling [super viewDidLoad] before you set the subclass’ title and prompt or else they’ll be reset on your behalf, infuriatingly.

And you’re not repeating yourself anyway, right? It’s not like you’d ever use these values anywhere else; you’d use different values. Not repetition at all.

Of course, while the content varies, the actions do not. You’re duplicating the assignment of these two related properties. It is repetition; but it appears to be almost unavoidable repetition. Oh, you could parameterize the method with two strings, but what has that got you, really? At best a convenience that you will, again, use at most once or twice and which saves you, ultimately, nothing. As with -setChild:, we’re left with discomfort: users have to know how it works to be able to use it or the class containing it safely.

The commonality this shares with -setChild: is also the contrast it has with -firstObject: -setChild: and -setUpNavigationItem both tell the computer how to do something. -firstObject, on the other hand, defines what the first object is.

Which is to say: of course -firstObject also tells the computer how to accomplish its task: send the count message to self, and if its return value was nonzero then return the result of sending -objectAtIndexedSubscript: to self with a parameter of zero; otherwise, return nil. But this recipe for how the computer should perform it coincides nicely with its declaration of what it is: “the first object of arrays is, when nonempty the one at object 0, and otherwise nil.”

With -setUpNavigationItem, defining the what is a contradiction in terms: it’s not a noun, not a concept. It has no value (by which I mean that it declares its return type to be void, i.e. no type, no value). It was abstracted, but in a sense it is not an abstraction, but merely an extraction of specific instructions.

Declarative programming is nebulous, ill-defined, abstract. For the moment let’s characterize it as the -firstObject approach, whereas the setUpNavigationItem and -setChild: approaches are imperative. Or, more succinctly: the difference between what and how.

But what -firstObject is boils down to semantics, surely. We don’t have access to the syntax tree at runtime; we don’t have Lisp-style macros that can operate on the messages at or before compile-time, either. Nothing about these messages is available at any point to us, so all that they can do is tell the computer how it should behave! Right?

In fact, yes. The specific messages or structure of messages are (basically) irrelevant. It really is semantics. And of course “semantics” means “meaning.” So where an abstraction is a concept named and made available to us for use across varying specific details (in -firstObject’s case, which array we are getting the first object of), the fact that it is declarative tells us that its name has meaning. In this sense, too, -setChild: and -setUpNavigationItem are not abstractions, despite having been abstracted: they have no meaning, only instructions. They are not nouns. They are not concepts.

They do not exist. They are unreal, unthings.

This is what happens to you when you stare too long into the void.

While -firstObject is an abstraction, it can be abstracted further. For example, we could generalize it to return the object at any in-bounds ordinal or nil:

-(id)nthObject:(NSUInteger)n {
	return (self.count > n)? self[n] : nil;
}

Now we can define -firstObject in terms of -nthObject::

-(id)firstObject {
   	return [self nthObject:0];
}

Clearly, -nthObject: is more abstract than -firstObject: it includes one fewer concrete concept, but can still implement the same behaviour. Can we abstract it further?

One way to identify a potential abstraction is simply to consider some decision that the code in question is making and simply take the result of that decision as a parameter. -nthObject: is making several decisions: it decides whether to return a member or nil; it decides to return a specific value in the in-bounds case; and it decides to return a specific value (nil) in the out-of-bounds case. Let’s consider this last one. Extracting it into a parameter, we get:

-(id)nthObject:(NSUInteger)n default:(id)marker {
	return (self.count > n)? self[n] : marker;
}
-(id)nthObject:(NSUInteger)n {
	return [self nthObject:n default:nil];
}

This is, again, clearly more abstract: we generalized from the return of nil to the return of some marker value.

In each of these instances, a decision was abstracted by replacing a constant with a parameter (and adjusting the logic accordingly where necessary). Each resulting method is more abstract than the previous. Are there other ways of abstracting?

I’m not going to answer this, or at least not right now. But take it as an experiment: Can you abstract a concept without replacing a constant with a variable (parameter, instance variable, or otherwise)? If so, what does that look like? And by all means if you do try this, let me know where it takes you. I know what my answer is, but it might not be the same as yours!

Part the Third: Complexity Complex

It might seem like a non sequitur to jump from talking about abstraction to talking about complexity, but I promise the segue makes sense if you’re me.

Has anyone here been programming for the Mac since before 2011 or for iOS since before 2012? Or has anyone written apps without autolayout and then tried writing one with autolayout?

How often did you swear?

Maybe you said to yourself: Why does autolayout have to make it so hard to just set the view’s frame? This was so easy with autoresizing masks! Why does autolayout have to make everything so complicated?

To be clear: this feeling is totally justified. It really was easy with frames and autoresizing masks. It really is less easy with autolayout. But what does that mean, precisely?

There’s an interesting contrast to be drawn between simplicity and ease. I am hardly the first to make this distinction, but I’d like to talk about it in terms of counting.

How many concrete concepts does -firstObject contain? When I say “concrete” I mean more or less “invariant”—concepts which are part of the contract of the method. Remember, this is, by definition, semantics, so we shouldn’t be afraid of this question just because it’s fuzzy. Let’s count them:

-(id)firstObject {
	return self.count? self[**0**] : **nil**;
}

(Notice that I’m using the original definition, before we abstracted it.)

I count (at least) two:

  1. The index of the object that we want to extract.

  2. The object that we want to return if the array is empty.

(As an aside, we could also consider the condition as being a concrete, or at least relatively concrete concept. For the purposes of this exercise, however, it is not hugely informative to do so.)

How about -nthObject:?

-(id)nthObject:(NSUInteger)n {
	return (self.count > n)? self[n] : nil;
}

Using the same metrics, it looks like only one:

  1. The object that we want to return if the desired index is out of bounds.

And -nthObject:default:?

-(id)nthObject:(NSUInteger)n default:(id)marker {
	return (self.count > n)? self[n] : marker;
}

By my count, 0. So it would seem that the more abstract the code, the fewer concrete concepts are involved. And indeed it makes some intuitive sense that abstraction and concreteness would be inversely linked.

Now let’s look at the definitions of -firstObject and -nthObject: in terms of their more concrete counterparts:

-(id)firstObject {
	return [self nthObject:0];
}

-(id)nthObject:(NSUInteger)n {
	return [self nthObject:n default:nil];
}

Looked at through the same lens, each of these definitions has captured only one concrete concept! And this is more than simply semantics: perhaps you noticed that when we defined -nthObject:default: and then redefined -nthObject: in terms of it, we didn’t need to change -firstObject at all! It stayed exactly the same.

This is getting near the heart of the difference between simplicity and ease. When you want the first object in an array (defaulting to nil if empty), is it easier to write the code out by hand or to use -nthObject: and pass 0? Obviously, -nthObject: is easier. Is it easier to use -nthObject:, again passing 0, or to use -firstObject? Again, obviously -firstObject is easier. But is it simpler?

When we looked at the original implementation of -firstObject, we counted two concrete concepts. When we looked at the redefinition, we counted only one. Where did the other one go? And why didn’t we have to change -firstObject when we changed -nthObject:?

Remember that the two concrete concepts in the original -firstObject were 0 and nil. The redefinition only included 0, however, because nil was moved inside -nthObject:! -firstObject is in fact using exactly as many concrete concepts as it was before, it’s just hidden the second one behind the call to nthObject:—behind the veil of abstraction. Where it belongs!

This, then, is the very core of simplicity: a simpler program has fewer concepts, a complex one has more. -firstObject is more complex than -nthObject: precisely because it is less general; the increase in abstraction mirrors an increase in simplicity.

Ease is in fact convenience. What makes it so hard to just set the view’s frame with autolayout? It’s not complexity; it’s inconvenience.

Let me qualify that a little further: If -firstObject is exactly as complex before and after the existence of -nthObject:, then isn’t it more complex to use autolayout constraints to define the frame of the view on the screen than to assign that frame?

If that’s all you ever want to do, then yes. But what’s involved in setting the frame when you want to ensure that it behaves according to a system of rules, e.g. that it is never larger than its parent, that it is never smaller than a certain size, that it is the same width as some other thing, that it has a certain aspect ratio?

The complexity of implementing all of this quickly exceeds that of using autolayout. The key is that what I will call the total complexity of -firstObject—that is, the number of concrete concepts used by either it or the abstractions it uses—remained the same, the local complexity—the number of direct references to concrete concepts—was reduced.

The apparent simplicity of -setFrame:—really its ease—doesn’t scale. Without an abstraction of the rules for how the frame of that view should behave, you have to implement them all by hand. You also have to ensure that they’re all enforced whenever their dependencies change. Whereas autolayout encompasses not only the rules embodied in the constraints, but also the necessary logic to keep them consistent and current—-layout[Subviews], -updateConstraints, and so on.

This sheds new light on -setUpNavigationItem. How many concrete concepts does it involve?

-(void)setUpNavigationItem {
	self.navigationItem.title = @"Recipient";
	self.navigationItem.prompt = @"Where would you like the flowers sent?";
}

Multiple choice:

  1. Wait, are we talking about total or local complexity here?
  2. At least 4.
  3. You’re clearly just going to tell us anyway so get on with it.

Good answer.

The values assigned to self.navigationItem.title and self.navigationItem.prompt are clearly both concrete concepts. But, crucially, so are self.navigationItem’s title and prompt!

Wait a minute. We didn’t count anything involving self in -firstObject or -nthObject:; what gives?

Masked by the setters is another concrete concept: the change to the navigation item inherent in assigning a variable to one of its properties. This may seem like a bit of a leap from counting constants, but remember that one of the difficulties with the usefulness of -setUpNavigationItem was that if you subclassed the class implementing it, you would have to think about the order in which methods which you might not even know about would be called in, in order to ensure that you did not encounter bugs. It’s not explicit in the syntax, but changes to state are real concepts which you have to think about in order to program in Objective-C.

This is because Objective-C, like many languages, is imperative in nature: you, the Grand Imperator, tell the program how to behave. In a declarative programming language, for example SQL, Prolog, or Haskell, you are instead the Grand Declarator, telling each abstraction what it is. Again: how vs. what.

In order to describe changes in state in a declarative programming language, you therefore have to make them explicit: tell the program what it is in terms of these changes. Any part of the program using them is therefore referring to at least one concrete concept, making it more complex than an equivalent part of the program not also using state changes.

In an imperative language, state changes are the status quo, the modus operandi, or even the mode de vie. You live and breathe them, and thus all of your code is made more complex.

At the same time, abstracting imperatively, abstracting-without-abstractions, denies you the ability to deal with these extra concepts behind the veil of local complexity; that is, any code using an imperative abstraction necessarily incurs the complexity of any changes it performs in a total sense (as with any other abstraction), but also incurs it locally—because any other changes to the same state need to be carefully sequenced. The details leak from callee to caller, again and again, and can never be contained.

Part the Somethingth: Flow Control

While Objective-C is an imperative language, we can view this as meaning that it can be programmed in an imperative style. Fortunately for us, we have seen that it can also be programmed in a declarative style. What would a declarative counterpart to -setUpNavigationItem look like?

First off, it would have to have a return value; it’s not an abstraction if it doesn’t exist. Since this is just a second approximation, let’s just return a dictionary:

-(NSDictionary *)

Wait a minute! Immediately we hit a wall. What do we call it? We’re basically describing how a UINavigationItem should be configured, but we can’t just call it -navigationItem and return a UINavigationItem * instead of a dictionary, can we? And even if we did, aren’t we just going to be calling setters on that, thus incurring more complexity?

To the first question: that sounds like a fun experiment! Let me know how it goes for you.

To the second question: If state is mutated in the forest and no one is around to observe its side effects, does it incur a penalty? Less cheekily: We’d definitely have to call setters on it. But how would anything know? If we allocate a new object, and assign some values to it, nothing outside of that method can see those individual changes. Only changes to an object that both methods use will cause problems. And of course nothing can use the returned object until it has been returned, by which point the method creating it will not be changing it again!

In the meantime, we’ll still just return a dictionary from this method; let’s call it -navigationItemState:

-(NSDictionary *)navigationItemState {
	return @{ @"title": @"Recipient",
	          @"prompt": @"Where would you like the flowers sent?" };
}

Now we have only the complexity of the values, not of the changes as well. But surely this is hand-waving: this doesn’t assign anything to the navigationItem; it’s just moved the problem elsewhere!

Indeed it has. The sad truth is that UIViewController’s API is not a particularly declarative one; we’re on our own for this one. Alright then, what does it look like to assign this, perhaps in -viewDidLoad?

NSDictionary *state = self.navigationItemState;
self.navigationItem.title = state[@"title"];
self.navigationItem.prompt = state[@"prompt"];

Now I know it’s just hand-waving: that’s exactly the kind of assignment we were trying to avoid!

Or is it? Since the change has been decoupled from the values, we can subclass this view controller with impunity, implementing -navigationItemState and returning our own values. We’re still out of luck if we don’t know it exists, of course; we would still be changing our own navigationItem’s properties in competition with [super viewDidLoad], and thus dependent on ordering. But this is a non-issue; it’s trivially obvious that a declarative API can’t help you if you don’t use it!

But what if we only want to show the navigation item’s prompt when in portrait, and never when in landscape? Should we implement -navigationItemStateForUserInterfaceOrientation:?

That might be a start; but we’d still have to make sure that we’re assigning the state manually every time the device’s orientation changes. Then if we decide we also want it to change when some other factor is changed, we’re forced to update it then, too, and carefully ensure that we apply all the relevant rules every time. Oh, and that it all happens on the main thread, if that’s a concern for any of our state transitions! It’s a lot like -setFrame: vs. autolayout.

Strike that: it’s exactly like -setFrame: vs. autolayout.

We’ve missed an opportunity for abstraction. We care about more than just the state, we also care about how it depends on the state around it. Maybe we encode these rules as lines of imperative code, or maybe we encode them in some declarative abstraction—something which we construct, and whose structure implies the desired behaviour.

We further need to encode when the state changes, i.e. in response to changes in what properties should the navigation item’s state change? This is an orthogonal concern, but clearly closely related: each of the navigation item state’s rules is based on some piece of state in some other object, for example the view controller’s orientation; let’s call these dependencies. Therefore, when the value of any dependency changes, so should the navigation item’s state.

When you put this all together, you have something like KVO. No wait, ReactiveCocoa! Or maybe Cocoa Bindings? Or maybe just a Memo object:

@interface Memo : NSObject
    
-(instancetype)initWithBlock:(id(^)())block;
    
-(void)addDependencyWithObject:(id)dependency keyPath:(NSString *)keyPath;
    
@property (readonly) id value;
    
@end

When you create a Memo, you give it a block that it should call when any of its dependencies is changed.

-addDependencyWithObject:keyPath: adds a dependency which it watches for changes, presumably with KVO.

When you call -value, it checks to see if it has been invalidated by a change to a dependency, and if so, evaluates the block. It returns the object that the block produced most recently.

Every Memo is effectively a cache of a calculated value that is automatically invalidated whenever one or more of its dependencies changes; it is updated lazily, when its value is requested.

This is not sufficient to cover the gamut of responses to state changes! It is perhaps instead the 90% solution: enough to cover the majority of cases. I’d encourage you to try implementing an object with this interface, and to use it and see when and why it falls short; if you’d like to see what I did, it’s in the aforementioned Navel-Gazing repository on my github account.

While it doesn’t cover everything, this minimal representation is sufficient to handle changes to the notification item’s state in response to changes in orientation:

-(NSDictionary *)navigationItemState {
   	return UIInterfaceOrientationIsPortrait([UIApplication sharedApplication].statusBarOrientation)?
   		@{ @"title": @"Recipient",
   		   @"prompt": @"Where would you like the flowers sent?" }
   	:	@{ @"title": @"Recipient" };
}

 -(void)viewDidLoad {
    Memo *memo = [[Memo alloc] initWithBlock:^{
    	NSDictionary *state = self.navigationItemState;
    	self.navigationItem.title = state[@"title"];
    	self.navigationItem.prompt = state[@"prompt"];
    	return state;
    }];
    [memo addDependencyWithObject:[UIApplication sharedApplication] keyPath:@"statusBarOrientation"];
}

But if you try this, it doesn’t work. What’s wrong? Memos as described above are lazy, but since we’re never using the memo’s value, it’s never being calculated. Once again, the impedance mismatch between imperative and declarative styles rears its ugly head.

One way to bridge the gap would be to implement strict, or greedy evaluation, forcing the value to be updated as soon as it is invalidated. (I’ll leave that as an exercise for the audience.) But taking a step back, it’s useful to realize that this is, again, a problem where imperative code forces us to care about ordering. Pushing this imperative code to the margins will help the rest of your code be cleaner, easier to reason about; but you can never root it out completely. It taints anything which calls it.

On the other hand, if we were always using the value of the memo at all the right times, for example if internally UIViewController was using some mechanism to observe changes to our memo for updates—because of course every Memo’s value is KVO-compliant!—we would see exactly the desired behaviour. The problems with ordering and timing would disappear.

In fact, the way we use Memo above is acting in a role that a reactive programming glossary might refer to as a sink. While Memo is capable of acting as what’s called a signal, which responds to changes in its dependencies by computing new values on demand, it’s being used here solely to update the navigation item imperatively in response to changes. Again, we don’t use its value, so we don’t run its block. Conflating the imperative how this memo computes with the declarative what this memo represents has caused this bug. Worse, it’s caused us to consider complicating the abstraction with a different evaluation scheme in order to fix it. Perhaps we don’t need to change it at all.

Where a signal is declarative, producing new values when necessary and demanded of it, a sink is imperative, performing some imperative side effects as soon as it’s invalidated. Can we push these imperative effects further into the margins? Memo takes us from change to value; maybe we need an abstraction to take us from change to effect. Let’s call it a Sink.

@interface Sink : NSObject
    
-(instancetype)initWithBlock:(void(^)())block;
    
-(void)addDependencyWithObject:(id)dependency keyPath:(NSString *)keyPath;
    
@end

There are strong similarities with Memo, and maybe we should think about abstracting the dependency handling out; but for the moment let’s assume that this is someone else’s problem. Instead of adding greedy evaluation to Memo, we’ll add a Sink which just calls a block when its dependencies invalidate it; no value will be returned, since we don’t need one to call the imperative code.

-(void)viewDidLoad {
	UIApplication *app = [UIApplication sharedApplication];
	UIInterfaceOrientation orientation = app.statusBarOrientation;
	Memo *memo = [[Memo alloc] initWithBlock:^{
		return [self navigationItemStateForOrientation:orientation];
	}];
	[memo addDependencyWithObject:app keyPath:@"statusBarOrientation"];
    	
	Sink *sink = [[Sink alloc] initWithBlock:^{
		NSDictionary *state = memo.value;
		self.navigationItem.title = state[@"title"];
		self.navigationItem.prompt = state[@"prompt"];
    }];
    [sink addDependencyWithObject:memo keyPath:@"value"];
}

Now, when the status bar orientation changes, Memo is invalidated, which in turn immediately invalidates its value and notifies any observers via KVO. This triggers the sink to run its block and ask memo for its value, and when it does so, memo is evaluated.

Turning to the evaluation of the memo, we see that we’ve also now abstracted the orientation into a parameter, such that the -navigationItemStateForOrientation: method no longer needs to know about the UIApplication; removing the reference to the singleton means that there’s one fewer tightly coupled symbol, and ergo less complication. This method isn’t shown, but adding the parameter is straightforward.

We could go a step further, abstracting the dependency tracking, perhaps even providing the dependencies’ latest values to the memo such that it no longer has to duplicate the dependency key path and fetching the state. If the dependency tracking memoizes these values, then it looks more and more like a Memo itself, and we might think about making it a protocol instead of just a class interface. But for now, we will move along to the sink.

Having retrieved memo’s value, sink applies it to the navigation item; and, as expected, the problems of timing and ordering have gone away.

With those problems goes some knowledge and control. We no longer have concrete knowledge or control of precisely when and how the state is calculated or used. It’s memoized, so it may not be calculated every time it’s used. If it’s lazy, so it may not be calculated when it’s _un_used. We don’t know how often the memoized value will be requested of us, because that’s coming from some other part of the code—or of some other person’s code.

It’s no coincidence that this loss of control and gain in precision go hand in hand; they are, in fact, one and the same. As abstraction increases, complexity decreases; and control flow is inherently complex. Specific control flow is the opposite of high abstraction. Per Kowalski, ALGORITHM = LOGIC + CONTROL; and in particular, declarative abstractions gradually abstract the control away, replacing it with structure.

Part the Nextth: Why Declarative?

All declarative systems are abstract, but perhaps not all abstractions are declarative. What makes an abstraction declarative?

As we saw, a return value goes a long ways: it gives us something to reason on. If that is declaring what the concept of the method is, and if some abstractions are more abstract than others, then maybe we could extrapolate that some such declarations are more declarative than others.

For example, -firstObject is clearly declarative code, but it doesn’t give us as much useful to reason on as Memo or even partly-imperative Sink does—it has meaning, but it has less meaning than they do; after all, you can only refer to it in the abstract to a certain point—you must always be speaking of the array whose first object this is.

Instances of Memo and Sink, on the other hand, are values by nature, where methods are less so (not being truly first-class concepts in the language). As we have seen, they can be combined with other objects quite flexibly. It does not seem too much of a stretch to describe them as being more declarative than, say, the average app delegate or those view controllers you sometimes see where it’s clear the developers wasn’t going to add one single other controller class to the project without a gun to their head.

This is a bit off in the weeds; I’m not sure you’ll agree with me, and I’m not sure anyone else will either. (Take that as an imperative, if you will, to test this declaration.) But part of what I think about when I think of “declarative programming” is the notion that you are constructing a system of objects at runtime out of which the desired behaviour falls naturally: a necessary consequence of the structure.

To give an example of this, the appearance of a view hierarchy is a consequence of the structure of the views, and their arrangement and visual properties. All of these are declared properties of the view controllers; in this sense, we can think of a view hierarchy as being a declarative system, whose value is the contents of the layer, or perhaps the screen buffer.

View controllers have a similar hierarchy which provides the semantic structure of the app; this, and the paths through which the user will be taken at runtime, is the structure made explicit in storyboards.

Views and view controllers are, basically, a solved problem—Apple has provided these things, and by and large we use them. It’s important to realize, however, that they have not cornered the market. We solve problems all day every day, generally more than we cause; that’s why anyone bothers to pay us. Some of these problems are likely to have declarative solutions (after all, if you can’t talk about what these things are, then what are they, really?); some of those declarative solutions may well be qualitatively better than how we might be solving them otherwise—in fact, I think that’s extremely likely, given what we’ve seen about their behaviour with regard to time and ordering relative to imperative solutions.

How can we find such solutions?

By pursuing simplicity.

Part the nth: The Composition of Abstraction

The Structure and Interpretation of Computer Programs, in my opinion the most important work on our field yet produced, tells us early on that the most important features of every programming language are its facilities for abstraction and composition, and that we should therefore judge every language by these features.

This is hyperbole, but I think it’s justified hyperbole. Composition is abstraction’s dual; where abstraction is breaking a problem into simpler components, composition is reassembling those into the solution. Constructing an abstraction is generally itself composition of other abstractions; any time you use an abstraction, you are composing.

Where abstraction reduces complexity, composition increases it. This is warranted, of course; if you have a complex problem, then the solution—the negative space around the problem, if you will, sharing its surface with all its fractal knots and whorls—must likewise be complex!

Fortunately, imperative composition and declarative composition model the complexity differently. Imperative composition models the complexity in the structure of the code, with every piece of the solution expressed explicitly in the control flow. Declarative composition models the complexity in the structure of the object graph at runtime, with every piece of the solution implied in the code and how the structure is built—and in effect, how it is automated, since we rarely build an object graph by hand.

Imperative composition is the approach we’re all familiar with; we write code to do one thing, and then to do another thing. Perhaps we write a loop. This is -setUpNavigationItem or -setChild:—imperative code, ordered, normal, and regrettably fragile.

Declarative composition, however, is the assembly of declarative abstractions by appending or alternating declarative abstractions with other declarative abstractions. An array is a declarative abstraction; so is a tree node; or a view (with subviews); or a view controller (with child view controllers); or an autolayout constraint with its items. So is a Memo with dependencies that turn out to themselves be other Memos, if we take that route. And for much the same reason, so is a Sink, despite its imperative process.

Even though a declarative solution will model the complexity in the structure of the object graph and not in the structure of the code, managing this complexity is important—this is, after all, a key responsibility of every programmer who wishes to continue solving problems tomorrow without having to rewrite all of their code from scratch. We need to be able to debug, we need to have acceptable performance, and we need, fundamentally, to understand what’s going on—to have a mental model.

It is therefore in our best interests to ensure that the abstractions we build are as simple as possible: simple abstractions are more flexible, meaning more easily composed together, because they do not introduce factors not necessary to their operation.

Each instance variable you add to a class increases its complexity. Each reference to a singleton increases its complexity. Each reference to a global variable increases its complexity. Each line of code increases its complexity.

There is no analogous cost to adding classes to the project. We may feel like we incur some terrible penalty to have to add a new class, but as with integers, these things aren’t rationed; we aren’t in danger of exhausting a 32-bit address space with classes any time soon.

Ergo, more, simpler classes are “better”—in the sense of allowing “more declarative” solutions—than are fewer, more complex classes. And at this scale, we can use lines of code as a rule of thumb: longer classes are probably more complex, and are less likely to be adequately declarative.

Memo is fairly minimalistic. We could factor out the observation of dependencies—that’s orthogonal to the core invalidate/evaluate/value concept, and could perhaps be used by Sink. We could have the dependencies themselves be Memos, giving this operation closure. However, we see diminishing returns at some point; while I might seem to sneer at convenience, it’s ultimately what pays Apple’s bills, and mine, and likely yours. As a declarative programmer, you are designing APIs; as an API designer, developers are your users. And users are people too!

In the end, you have to ship. Experimenting with these ideas will not help you ship on Monday. Experimenting with these ideas will not help you ship next week. Probably not next month. But three to six months from now, having this experimentation behind you, having experience with these ideas in your repertoire, it absolutely will.

Declarative code is simpler. It’s usually easier to read, in my experience. It’s smaller, if more spread out; easy to get around. It’s more reliable simply because each component has fewer moving pieces which could fail, and fewer joints between them that could break. On top of all of that, the lack of ordering means that declarative code is immune to race conditions, the single largest issue with concurrency. And it is clearly the direction that the market and the industry is heading in: there is a lot of ongoing, exciting research being done in declarative languages and systems and how to survive in an imperative world.

Further, Apple is increasingly implementing declarative APIs, and being able to use them isn’t enough; if we are to be able to tackle harder and harder problems, if we are to learn and grow, and if we are to stay relevant in the market, we need to be able to write them.

But again, it’s important to remember that there is no silver bullet. You will run into problems along the way:

Declarative code can be slower than the imperative code you might have written—but it can also be faster, since having the structure available at runtime might give you hints about how the objects are used which could help you to automatically organize them in memory for better locality of reference. Compilers do this with the syntax trees they build; that’s approximately what optimizers are. We can do so too.

Declarative code can be harder to debug than the imperative code you might have written—but it can also be easier, since you have the structure at runtime to look at; you can analyze the state of the graph as a whole instead of just stepping through it. It’s necessarily different, of course; if control flow is abstracted, it becomes increasingly difficult for your the API’s client to simply use lldb to debug it.

Declarative code can use more memory than the imperative code you might have written—but it can also use less. Once again, the increase in context could be leveraged to allow you to optimize for space as well as time.

Declarative code can be less familiar than the imperative code you might have written. Only time and practice will fix this one.

No declarative programming techniques will solve these problems for you; they may in fact require you to solve them for your API’s clients, where before they might have been your clients’ problem. This is a responsibility, but it’s also an opportunity, as is so often the case: API design is not a very common expertise in the market.

There may also be problems that don’t lend themselves to a declarative solution. Even if a particular problem does, your particular solution may not allow for some particular functionality or flexibility that some particular client requires. Every abstraction reduces the potential computation that can be done with it by some small amount; unless your abstraction is a Turing-complete language, it will necessarily reduce the flexibility of its clients to some degree. This is fine; this is, in fact, the point. When you can do everything, doing anything at all requires a lot of time and effort—and really, that’s what we’re here for.

Epilogue: Abstraction in the Abstract

There is a proverb about a man who points at the moon, and says “There is the moon.” We do not think he is referring to his finger, and so the lesson is: “Do not mistake the pointing finger for the moon.”

Really, his finger is a symbol, a stand-in, to avoid his having to take you to the moon and smack you over the head with it before you get the point. The moon is a concrete thing; his finger, and indeed the word “moon,” are abstractions. We can be even more abstract: other worlds have moons, too. The moon is really just a moon. This is my moon; there are many like it, but this one is mine.

(Why are we uncomfortable using articles and pronouns in method names?)

To abstract is to identify an idea, a concept, as a unique thing which can be reasoned and acted upon in isolation. It is to give it a name; to define that name with that concept. This is equally true in language and in code.

We abstract because it’s difficult to reason about something which we do not have a word for. Sometimes we don’t have good enough words for things that are important to us: the feeling of summer’s warmth passing as fall’s crispness enters the air; or of the bittersweet experience of a trusted colleague and friend leaving to work on something important to them: that simultaneous grief at your loss and joy at their gain. Or how the joy outweighs the grief.

We have these concepts, but it’s hard to think or talk about them without the symbols for them, the words. Abstracting gives us what we lack. We name unique objects, and categories of objects, and also concepts.

Sometimes these are new words, like zeitgeist or monad. These are never new ideas, however: we cannot name an idea we have not had. Even if we are the first and only to think of an idea, the selection of the word must follow, never lead. So it is with our software: we must always abstract from something we already know, and this is done most easily with working code.

If abstraction is vocabulary, then composition is grammar: phrasing our abstractions together such that we can apply them to connote meaning (and behaviour!). Using this most effectively is going to require at least as much practice, hand-in-hand with abstraction: they are yin and yang, simultaneously candlestick and faces, negative space and figure.

So if it ain’t broke, you aren’t trying hard enough yet. It’s going to be hard and uncomfortable and it might make you unpopular if you do it at work right before a deadline, but it’s the only way to master the skills, and the only way to apply these skills once mastered to concepts you were previously unfamiliar with.

By all means, therefore, make mistakes. Break things.

Just do so on a branch.

Q?&A!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment