Choosing the Best Expression

A recent objc.io post on Phantom Types included the following snippet:

struct FileHandle<A> {
    let handle: NSFileHandle
}
struct Read {}

func openForReading(path: String) -> FileHandle<Read>? {
    return NSFileHandle(forReadingAtPath: path).map { FileHandle(handle: $0) }
}

Instead of using map here, I feel that an if-let would be more appropriate. If I were writing openForReading(path:), I would have written it like this:

func openForReading(path: String) -> FileHandle<Read>? {
  if let fh = NSFileHandle(forReadingAtPath: path) {
    return FileHandle(handle: fh)
  } else {
    return nil
  }
}

Sure, it‘s a bit more verbose, but I feel like it‘s a better expression of the program‘s meaning.

When a programming language gives you multiple ways of expressing the same idea, the expression the programmer chooses will highlight a specific aspect of that approach. Here, the map expression is effectively a choice to emphasize the definition of a function whose domain is the possible return values of the NSFileHandle initializer. Because the domain of a function is a set, the reader is suddenly concerned that there is a collection of NSFileHandles being mapped into FileHandles!

Understanding what this function actually does requires two pieces of background information: that NSFileHandle.init(forReadingAtPath:) returns an Optional (so it may return nil if given a bad path), and that Optional<T>.map exists to treat a non-nil Optional as if it were a one-element set. Then it becomes apparent that the writer‘s intent is to transform only vaild NSFileHandles into FileHandle<Read>s.

Surely programmers cannot be absolved from having some knowledge of the framework and the standard library they are using, but let’s contrast this approach with the alternative. The if-let formulation emphasizes the specific elements for which execution is defined. It also specifically highlights that one of those elements is nil, and thus it‘s fairly apparent to the reader that the purpose of the construct is to define correct behavior for success and failure cases. No need to have memorized whether NSFileHandle has a failable initializer. No need to know that the Optional<T>.map shortcut exists.

That said, we do lose the assurance that all possible cases are covered, which map provides. We could gain that back with a (never-executed) else clause, or we can rely on the explicitness of the nil test to assure the reader that only two possible cases exist and that both are covered. Percentage-wise, we also gain a fair bit of verbosity, but in absolute terms it‘s not so much. In fact, I‘d argue that the map version is actually compressing too much information into too few characters.

Fortunately, we can combine if-let‘s readability with map‘s terseness and assurance of having defined a total function by defining a function-pipelining operator |->? like this:

infix operator |->? { associativity right }
public func |->?<T, U> (v: T?, f: T? -> U?) {
  if v == nil {
    return nil
  } else {
    return f(v)
  }
}

One can imagine that this operator is the sibling of a |-> which does not deal in Optionals. The trailing ? makes the behavior of this operator clear to the reader by analogy to the ?. (optional-chaining) and ?? (nil-coalescing) operators.

Now we can rewrite openForReading(path:) like this:

func openForReading(path: String) -> FileHandle<Read>? {
  return NSFileHandle(forReadingAtPath: path) |->? { FileHandle(handle: $0) }
}

Of course, we could convert the anonymous closure into a named function, perhaps named something like makeReadHandle. That minimizes the complexity of each element of the pipeline. Here, it might not be that big of an advantage, but if I stuck another step on the end of the pipeline, I‘d consider it rather strongly.