Swift, Protocols, and Composition

Ash Furrow wrote up this post yesterday: http://ashfurrow.com/blog/protocols-and-swift/. It's a good post, you should read it. The problem he outlines is this:

"Here's the problem: you have two types that want to talk to each other."

Ash's solution to the problem was to use protocols to essentially turn the problem inside out. His solution definitely works and it's better than his original way of doing it. It did get me thinking about solving the same problem in maybe a slightly more flexible way.

The drawback with Ash's solution, in my opinion, is that it still takes a heavy leaning on an object-oriented paradigm. I don't think we need that, especially in this case.

If we break down the problem into parts, I think we end up with this:

  1. A set of nutritional information (calories and hydration level). 2. Types of food (wet and dry) that can affect that nutritional information. 3. An animal type (cat) that has a set of nutritional information and the

ability to eat food.

The issue with the object-oriented approach is that it coupled the notion of eating with the animal type (see the eat extension). I don't think we really want that. Instead, we want to capture the relationship between food and nutrition through some eat function.

Let's setup the types:

enum Food {
    case Dry(caloriesPerGram: Double)
    case Wet(caloriesPerGram: Double, hydrationPerGram: Double)
}

protocol Nutrition {
    var calorieCount: Double { get set }
    var hydrationLevel: Double { get set }
}

struct Cat: Nutrition {
    var calorieCount: Double = 0
    var hydrationLevel: Double = 0
}

Ok, so what do we have:

  • The Food type is an enum because we have clear restrictions on the type of

food available and how it impacts Nutrition.

  • The Nutrition item is a protocol that defines the data that it needs. This

is done because we are saying that a cat "has a" set of nutrition information.

  • The Cat is simply a data type that is made up of Nutrition data.

The last thing we are missing is the ability to eat. If you are thinking about objects then I think the obvious thing to do is to add an eat function on

Nutrition. There is another approach though: thinking instead about the transformations of the data.

Because I care more about transforming data, I'm going to create a top-level function eat:

func eat(var nutrition: Nutrition, food: Food, grams: Double) -> Nutrition {
    switch food {
    case let .Dry(caloriesPerGram):
        nutrition.calorieCount += caloriesPerGram * grams
    case let .Wet(caloriesPerGram, hydrationPerGram):
        nutrition.calorieCount += caloriesPerGram * grams
        nutrition.hydrationLevel += hydrationPerGram * grams
    }

    return nutrition
}

func eat<T: Nutrition>(nutrition: T, food: Food, grams: Double) -> T {
    return eat(nutrition, food, grams) as T
}

There are a few things to note:

  1. The var usage in the parameter list just let's us get a copy of that

parameter and use it locally as a new variable. 2. There is absolutely no coupling with any of the types that could possibly

contain Nutrition information. 3. There is a generic version so we do not have to cast when dealing with

concrete implementations of Nutrition, such as Cat.

The usage of the code looks like this:

let kibble = Food.Dry(caloriesPerGram: 40)
let fancyFeast = Food.Wet(caloriesPerGram: 80, hydrationPerGram: 0.2)

var dave = Cat()
dave = eat(dave, kibble, 30)
dave = eat(dave, fancyFeast, 20)

And then we can do some other more functional oriented operations a bit more naturally:

let cats = [Cat(), Cat(), Cat(), Cat(), Cat(), Cat(), Cat()]
let caloriesEatenByCats = reduce(map(cats) { cat in
    eat(cat, kibble, 30) }, 0) { sum, cat in
        sum + cat.calorieCount }

But here is where the really nice part of this choice comes along: if I want to add a Dog type, I only need to create the struct for it I'm done.

struct Dog: Nutrition {
    var calorieCount: Double = 0
    var hydrationLevel: Double = 0
}

let dogs = [Dog(), Dog(), Dog(), Dog()]
let caloriesEatenByDogs = reduce(map(dogs) { dog in
    eat(dog, kibble, 30) }, 0) { sum, dog in
        sum + dog.calorieCount }

In Ash's implementation, you have to then go add an eat function on each new type you are dealing with. We lose no flexility either because if a Dog is supposed to eat differently, we can just create an overload for it.

The other nice part is we can mix and match our Nutrition types like this:

let animals: [Nutrition] = [Dog(), Cat()]
let caloriesEatenByAnimals = reduce(map(animals) { animal in
    eat(animal, kibble, 30) }, 0) { sum, animal in
        sum + animal.calorieCount }

It's just another way of thinking about the problem. Swift seems to be a language that values immutability and composition over mutability and if we start to think about solving problems that way, I think we end up with more solutions like I presented where it's really just about how we manipulate the data in composable ways.

Source: https://gist.github.com/owensd/7bda3f7b3f91245e3542

Swift, Protocols, and Composition