Stasis

So this static typing thing. Statically-dispatched protocols, all that. Why are they the way they are? Why do we bother? What are the pros and cons?

I’ve written a little about this before, but that post was code-light. So I want to make the case in a code-heavy kind of way.

Brent Simmons makes the best case (or the best case I can understand) for a different way of doing protocols. It’s centred on the example of heterogenous collections (although the points he makes are widely applicable): there are several use-cases where you want to have different objects collected together, where their only thing in common is some variable or function. Say they all have a “name” property, or they can all produce a hashValue.

This thing that they have in common: it seems like a perfect fit for protocols. I mean, if the thing that they have in common is that they’re all Hashable, you should be able to make a Set of them, right?

I don’t know. Really: the people coming over from Objective-C have some strategies that I’m not familiar with, and I’m not qualified to judge their usefulness. But I do know that there’s a completely different way to think about this problem.

Wipe the slate clean a second: forget about all those cool things you’ve heard about protocols and all the rest of it. For a minute, protocols are for one thing: code reuse. At their core, they allow composition instead of inheritance. They are not types, or classes, or structs. They only define the way a type relates to the functions that act on it – not the way a type relates to other types that conform.

You can take this idea as far as you want: hey, let’s stop CollectionType from inheriting from SequenceType. From now on, a collection is just a bunch of zero or more things. And a sequence is just something that gives you things, one at a time. Want to perform some function on everything in a collection? map, or forEach. No ordering, though: a collection is just a sloshy soup of some things. A sequence, though: that returns one thing at a time.

Why would you do this? Because now you’re treating protocols as abilities, not types. For instance, String can conform to CollectionType again! No more problems with variable-length characters, or right-to-left vs left-to-right: the sloshy soup model fits perfectly. Also, Dictionary and Set can conform: but without the weird semi-sliceableness, or the first and last properties that didn’t make sense.

Combine those two, though, and you get OrderedCollectionType! Something that you can walk along more than once, or append to, or whatever.

Using protocols like this makes them completely different from types. An even better example is CustomDebugStringConvertible: it represents the ability to do one thing: print. Nothing about types that conform to it is in any other way similar.

In this mode of thought, a collection of a protocol makes less sense. But what are you supposed to do? Brent Simmons gave the example of a file manager: folders will contain a whole bunch of things, some of which are files, others which are folders. You want to be able to store them in some CollectionType, but you also want them to have common properties: maybe a name, or size, or something. Here’s how I’d do that statically:

enum FileObject {
  case File
  case Folder(contents: [String:FileObject])
}

Here, folders hold a dictionary of FileObjects, with the keys being the names of those files. You could instead have it that each file contains its own name:

enum FileObject {
  case File(name: String)
  case Folder(name: String, contents: Set<FileObject>)
}

func ==(lhs: FileObject, rhs: FileObject) -> Bool {
  return lhs.hashValue == rhs.hashValue
}

extension FileObject : Hashable {
  var hashValue: Int {
    switch self {
    case let .File(name): return name.hashValue
    case let .Folder(name, _): return name.hashValue
    }
  }
}

But as you can see, it gets a little less elegant.

So what’s common between those two things? So far, only the fact that they each have a hashValue. But you can add other things as well: a thumbnail, say. Both a file and a folder would have a thumbnail:

enum FileObject {
  case File(image: ImageType)
  case Folder(image: ImageType, contents: [String:FileObject])
}

And, to return it:

extension FileObject {
  var thumbnail: ImageType {
    switch self {
    case let .File(pic): return pic
    case let . Folder(pic, _): return pic
  }
}

So now we could have a whole bunch of functions that operate with FileObjects, that can retrieve their thumbnails. We can build some complex functionality that works with those thumbnails.

Maybe, though, in the future, you change your mind. Maybe you want to do the finder in a completely different way. Hasn’t this static typing painted you into a corner? Everything about what you’ve written is hardcoded, not dynamic. Surely that means headache for any kind of extensibility?

Here’s where I differ. I think the hardcoded-ness makes it more extendable. Let’s say I wanted to add a new FileObject: a hidden file, say. You’d scroll all the way back to the top of your project, find your definition of FileObject, and change it:

enum FileObject {
  case File(image: ImageType)
  case Folder(image: ImageType, contents: [String:FileObject])
  case Hidden
}

Here’s why it’s useful: cascading through your entire project, little red stop signs will appear. Don’t think of these as errors: think of them as pedantic, curmudgeonly, helpful reminders. (well, binding reminders. But reminders nonetheless!)

You know the kind of things you should think about when writing unit tests? “Yeah, but what if the user’s name changes?”, or “Yeah, but what if the time zone changes on the stroke of midnight while crossing the international date line on a leap year during daylight savings?” The edge cases. Those little red stop signs are kindly, helpfully telling you about (some of) those edge cases.

Since you’ve added a new enum case, your switch statements that didn’t have a default (good reason to avoid default in a switch here) will now not work. Your thumbnail, for instance:

extension FileObject {
  var thumbnail: ImageType {
    switch self {
    case let .File(pic): return pic
    case let . Folder(pic, _): return pic
    // ERROR
  }
}

But this is what you want. I even like the wording here: in the case of a Hidden file, what should you return? Should you return anything at all?

The key here is to build your types logically: an object in the finder can be either a file, folder, or hidden. It makes sense that a hidden object doesn’t have a thumbnail. Let’s say every file can be tagged with an integer, though:

enum FileObject {
  case File(image: ImageType, tag: Int)
  case Folder(image: ImageType, contents: [String:FileObject], tag: Int)
  case Hidden(tag: Int)
}

Using a property, the rest of your code will still work fine:

extension FileObject {
  var tag: Int {
    switch self {
    case let .File(_, n): return n
    case let .Folder(_, n, _): return n
    case let .Hidden(n): return n
    }
  }
}

I don’t want you to think that I’m saying this way is better than the dynamic way. I’m not: I don’t know the dynamic way. I’m talking only about the tradeoff. Static typing isn’t meant to trade speed for extensibility (really really), or even safety for extensibility. It’s meant to trade one kind of extensibility for another. Whether it succeeds or not is a separate issue. Obviously the Swift version isn’t perfect: it won’t catch every bug, and the type system will bother you when there’s no need. But its intention is clear: Swift wants you to be able to change things, and it only wants to break the things that need breaking. Yes, you won’t be allowed to compile code that asks for a thumbnail from a hidden file. That’s the idea.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s