kelan.io

Approachable Functional Thinking, Using Protocol Extensions

I really enjoyed Daniel Steinberg‘s talk from NSConf 2015 – Somewhere between Tomorrowland and Frontierland. It gave me hope for being able to learn and apply some of the appealing techniques that I see Functional programmers rave about, without having to totally abandon my experience thinking in an Object-Oriented style.

This post is my attempt to explain this hopeful feeling, and to take Steinberg’s ideas even one step further, with the possibilities opened up by Swift 2.

In his talk, Steinberg explores a middle ground between the Functional and Object-Oriented styles of thinking. He does this by examining the “Battleship” example from the beginning of the objc.io Functional Programming in Swift book, in which the authors compare two approaches to answering the question, “Is a given target point within range of the battleship?”.

The first approach in the book uses a single method on a Battleship class to answer that question. The implementation of this method starts simple, but then evolves (rather poorly, to their point) over time as additional requirements are added. Steinberg calls this the “Object Oriented” approach, because a single object holds all the state, so all the logic gets shoved into that single place in the code.

The book then shows a different solution (which Steinberg calls the “Functional approach”), that takes advantage of first-class functions. This allows the code to be a lot clearer, using simple pieces that can be composed and transformed to build the final solution.

I remember being intrigued by this chapter in the book, because the clarity and composability of the functional approach was very appealing. But, I also felt like that way of thinking didn’t come readily to me. I didn’t have a way to describe it at the time, but I didn’t think I would have been able to come up with solutions like that on my own, and I wasn’t really sure how to “practice” that to get better at it.

So, that’s why Steinberg’s talk was so exciting for me. It seemed like he had the same feeling, and not only that, he had an alternate way of thinking about the solution (which he called the “intermediate approach”) that felt much more familiar to me.

The Intermediate Approach

To give a brief summary: Steinberg’s intermediate approach replaced the function used as the “composable building block” by the Functional approach with a Protocol, which could be adopted by a class and easily understood, but still be easily composed.

I’m not going to go through all the details here. You should definitely watch the talk and read the book (at least the “Thinking Functionally” chapter, which contains this example) to get up to speed. And, if you’re interested in this stuff, I highly recommend the rest of the book too.

One Step Better, using Protocol Extensions

After describing his intermediate approach, Steinberg tried to take it a step further to make even clearer code, but ran into a few unfortunate limitations of Swift (1.2, at least). He even specifically wished for a tool like Mixins (which I’m familiar with from Ruby) to help solve it in a better way.

Well, his wish was granted by Swift 2’s protocol extensions, so I wanted to see what that would look like, and I ended up being very pleased by the results, and am going to describe them here.

First, start with the Region protocol that Steinberg showed us.

protocol Region {
    func contains(position: Position) -> Bool
}

Our implementations can go back to being structs, which is nice (as opposed to the classes in Steinberg’s Inheritance approach.

struct Circle: Region {
    let radius: Distance
    func contains(position: Position) -> Bool {
        return hypot(position.x, position.y) <= radius
    }
}

struct Rectangle: Region {
    let width: Distance
    let height: Distance
    func contains(position: Position) -> Bool {
        return (fabs(position.x) <= width/2
            && fabs(position.y) <= height/2)
    }
}

struct ComposedRegion: Region {
    let containsRule: Position -> Bool  ///< holds the complex logic as a function
    func contains(position: Position) -> Bool {
        return containsRule(position)
    }
}

Next – and this is the neat part – implement the transformations as Protocol Extension on the Region protocol.

extension Region {
    func shift(by offset: Position) -> Region {
        func hitTest(point: Position) -> Bool {
            let shiftedPoint = Position(x: point.x - offset.x, y: point.y - offset.y)
            return self.contains(shiftedPoint)
        }
        return ComposedRegion(containsRule: hitTest)
    }

    func invert() -> Region {
        return ComposedRegion { point in !self.contains(point) }
    }

    func intersection(with other: Region) -> Region {
        return ComposedRegion(containsRule: { point in
            self.contains(point) && other.contains(point)
        })
    }

    func union(with other: Region) -> Region {
        return ComposedRegion(containsRule: { point in
            self.contains(point) || other.contains(point)
        })
    }

    func difference(minus minusRegion: Region) -> Region {
        return self.intersection(with: minusRegion.invert())
    }
}

Now we can marvel at the beauty of composing these.

let ownPosition = Position(x: 10, y: 12)
let weaponRange = Circle(radius: 5.0)
let safeDistance = Circle(radius: 1.0)
let friendlyRegion = safeDistance.shift(by: Position(x: 12, y: 9))

let shouldFireAtTarget = weaponRange
    .difference(minus: safeDistance)
    .shift(by: ownPosition)
    .difference(minus: friendlyRegion)

For reference, here was the solution using the pure Functional approach, from the objc.io book:

let ownPosition = Position(x: 10, y: 12)
let weaponRange = circle(5.0)
let safeDistance = circle(1.0)
let friendlyPosition = Position(x: 12, y: 9)
let friendlyRegion = shift(safeDistance, by: friendlyPosition)

let shouldFireAtTarget = difference(
    of: shift(
        difference(
            of: weaponRange,
            minusRegion: safeDistance),
        by: ownPosition),
    minusRegion: friendlyRegion)

Which, to me, is much harder to read and understand than the Protocol Extension approach, especially due to the “inside out” way that this one needs to be read.

Closing Thoughts

I think that the downfall of the original object-oriented approach came not from the fact that it was object-oriented per se, but instead from the fact that it didn’t define a thing that could then be readily transformed and composed. Instead, it was just a bunch of logic that was accreted onto an existing object – a single method, even. But, I do think this is an illustrative example that does happen commonly in practice, especially as new requirements trickle in to a project.

So, as Steinberg pointed out in his talk, the key insight here is defining the region as a thing; because then you can use functions to modify that thing. And, importantly, I now realize that you don’t have to full adopt a Functional way of thinking in order to accomplish this. You can follow Steinberg’s clever idea to use of a protocol as the thing, instead of a function.

In fact, I think something is lost when the thing is a function directly (e.g. if you do let unitCircle = circle(1.0)), because the naming of “unitCircle” only makes sense if you think about it as “this is a function evaluates to true for everything in the circle, and false for things outside”, but that’s not obvious from just the name “unitCircle”. So there is a lot of context you have to bring with you when reading the code.

Put another way, the functional approach uses “verbs” where I expect “nouns”. But the protocol gives me the noun back (even if it’s a noun that only has one verb).

But, we can also see how similar the intermediate and functional approaches are. When I presented this topic during one of our Swift lunchtime discussions at work, Jacob pointed out that the functional approach has:

typealias Region = Point -> Bool

whereas the protocol extension approach has:

protocol Region { func contains(Point) -> Bool }

Which is a nice confirmation of the parallels of these two approaches.

So, that’s what I love about Steinberg’s intermediate approach – it gives us the important “thing concept” as an actual thing, and still lets us have methods on that thing. And, with protocol extensions, those methods are just as easily composable as the functional approach (and, I think, even more readable in the end).

I hope this gives you some ideas for how it’s possible to reap the benefits of composability that come with a functional way of thinking, without having to give up all the years of object-oriented experience in your brain. I know it was an exciting realization for me.

Thanks

Thanks to Daniel Steinberg for his great talk, to the objc.io folks for the original problem that inspired this discussion, and to Jacob for helping to review this post, and the rest of the Swift Lunch participants at work.