Turn Your Code Into Poetry

Improving your code legibility with functional programming

let poem = myCode.turn(into: .poetry)
let poem = myCode.turn(into: .poetry)

Introduction

They say good code is like a book: it can be read like English. Well, what would you say about the following snippet of code?

let array = Array(1...100)
array.filter { $0 % 2 == 0}
let array = Array(1...100)
array.filter { $0 % 2 == 0 && $0 < 40 && $0 > 10}
let array = Array(1...100)
array.filter { $0 % 2 == 0 }
.filter { $0 < 40 }
.filter{ $0 > 10}
Running the code in an Xcode Playground shows the number of iterations each block take
Running the code in an Xcode Playground shows the number of iterations each block take
Running the code in an Xcode Playground

Case Study

Imagine we’re building a Person Classifier. We could have our individuals modeled by a struct Person, like this:

enum EyeColor {
case dark, blue, green, brown
}
enum HairColor {
case brunette, blonde, ginger, dark
}
struct Person {
var name: String
var eyesColor: EyeColor
var hairColor: HairColor
}
let people = [ ... ] 
let subset = people.filter { ($0.eyesColor == .green && $0.hairColor == .blonde) || ($0.eyesColor == .blue && $0.hairColor == .ginger) }

Filter

Let’s start by wrapping condition blocks into an object:

struct Filter<Element> {
typealias Condition = (Element) -> Bool
var condition: Condition
}
extension Array {
func filtering(_ filter: Filter<Element>) -> Array {
self.filter(filter.condition)
}
}
extension Filter {

struct Result {
private var matchingBlock: () -> Array<Element>
private var restBlock: () -> Array<Element>

var matching: Array<Element> { matchingBlock() }
var rest: Array<Element> { restBlock() }

init(matching: @escaping @autoclosure () -> Array<Element>,
rest: @escaping @autoclosure () -> Array<Element>) {
self.matchingBlock = matching
self.restBlock = rest
}
}

}
extension Array {
func filtering(_ filter: Filter<Element>) -> Filter<Element>.Result {
Filter.Result(matching: self.filter(filter.condition),
rest: self.filter((!filter).condition))
}
}
let subset = people.filter { $0.eyesColor == .blue }
let hasBlueEyes = Filter<Person> { $0.eyesColor == .blue }
let subset = people.filtering(hasBlueEyes).matching
let hasBlondeHair = Filter<Person> { $0.hairColor == .blonde }
let hasBlueEyes = Filter<Person> { $0.eyesColor == .blue }
let subset = people.filtering(hasBlueEyes).mathing
.filter(hasBlondeHair).matching

Adding Verbosity

In the previous section, we came to the conclusion that Filter lacked verbosity. There’s no way we can combine filters at the moment — let’s fix that.

var inverted: Self {
.init { !self.condition($0) }
}
func and(_ filter: Self) -> Self {
.init { filter.condition($0) && self.condition($0) }
}
func or(_ filter: Self) -> Self {
.init { filter.condition($0) || self.condition($0) }
}
let subset = people.filter(hasBlueEyes.and(hasBlondeHair).matching
struct Filter<Element> {
typealias Condition = (Element) -> Bool
var condition: Condition
}

extension Filter {

static var all: Self {
.init { _ in true }
}

static var none: Self {
.init { _ in false }
}

var inverted: Self {
.init { !self.condition($0) }
}

func and(_ filter: Self) -> Self {
.init { filter.condition($0) && self.condition($0) }
}

func or(_ filter: Self) -> Self {
.init { filter.condition($0) || self.condition($0) }
}

static prefix func ! (_ filter: Self) -> Self {
filter.inverted
}

static func & (_ lhs: Self, _ rhs: Self) -> Self {
lhs.and(rhs)
}

static func | (_ lhs: Self, _ rhs: Self) -> Self {
lhs.or(rhs)
}

static func any(of filters: Self...) -> Self {
Self.any(of: filters)
}

static func any(of filters: [Self]) -> Self {
filters.reduce(.none, |)
}

static func not(_ filters: Self...) -> Self {
Self.combine(filters.map { !$0 })
}

static func combine(_ filters: [Self]) -> Self {
filters.reduce(.all, &)
}

static func combine(_ filters: Self...) -> Self {
Self.combine(filters)
}

}
let hasBlondeHair = Filter<Person> { $0.hairColor == .blonde }
let hasBlueEyes = Filter<Person> { $0.eyesColor == .blue }
let result1 = people.filtering(!hasBlueEyes)
let result2 = people.filtering(hasBlueEyes & hasBlondeHair)
let result3 = people.filtering(hasBlueEyes | !hasBlondeHair)

Final Result

At this point, you have all the tools you need to take all that unmaintainable nonsense code and turn it into poetry. Want to see how? Let’s go back to our case study.

extension Filter where Element == Person {
static let brownEyes = Filter { $0.eyesColor == .brown }
static let blueEyes = Filter { $0.eyesColor == .blue }
static let darkEyes = Filter { $0.eyesColor == .dark }
static let greenEyes = Filter { $0.eyesColor == .green }

static let brunette = Filter { $0.hairColor == .brunette }
static let blonde = Filter { $0.hairColor == .blonde }
static let ginger = Filter { $0.hairColor == .ginger }
static let darkHair = Filter { $0.hairColor == .dark }

static func name(startingWith letter: String) -> Filter {
Filter { $0.name.starts(with: letter) }
}
}
let subset = people.filter { ($0.eyesColor == .green && $0.hairColor == .blonde) || ($0.eyesColor == .blue && $0.hairColor == .ginger) }
let subset = people.filtering(Filter.greenEyes.and(.blonde)
.or(Filter.ginger.and(.blueEyes))).matching
Shakespeare
Shakespeare
let people = [
Person(name: "Eliott", eyesColor: .brown, hairColor: .blonde),
Person(name: "Eoin", eyesColor: .brown, hairColor: .brunette),
Person(name: "Michelle", eyesColor: .brown, hairColor: .brunette),
Person(name: "Kevin", eyesColor: .blue, hairColor: .brunette),
Person(name: "Jessica", eyesColor: .green, hairColor: .brunette),
Person(name: "Thomas", eyesColor: .dark, hairColor: .dark),
Person(name: "Oliver", eyesColor: .dark, hairColor: .blonde),
Person(name: "Jane", eyesColor: .blue, hairColor: .ginger),
Person(name: "Justine", eyesColor: .brown, hairColor: .dark),
Person(name: "Joseph", eyesColor: .brown, hairColor: .brunette),
Person(name: "Michael", eyesColor: .blue, hairColor: .dark)
]

people.filtering(.combine(.name(startingWith: "E"), .brownEyes, .blonde)).matching // Eliott
people.filtering(.any(of: .ginger, .blonde, .greenEyes)).matching // Eliott, Jessica, Oliver, Jane
people.filtering(Filter.not(.name(startingWith: "J"), .brownEyes).and(.brunette)).matching // Kevin
people.filtering(Filter.greenEyes.and(.blonde).or(Filter.ginger.and(.blueEyes))).matching // Jane
enum DogBreed {
case pug
case husky
case boxer
case bulldog
case chowChow
}

struct Dog {
var name: String
var breed: DogBreed
}

let dog = [
Dog(name: "Rudolph", breed: .husky),
Dog(name: "Hugo", breed: .boxer),
Dog(name: "Trinity", breed: .pug),
Dog(name: "Neo", breed: .pug),
Dog(name: "Sammuel", breed: .chowChow),
Dog(name: "Princess", breed: .bulldog)
]

extension Filter where Element == Dog {
static let pug = Filter { $0.breed == .pug }
static let husky = Filter { $0.breed == .husky }
static let boxer = Filter { $0.breed == .boxer }
static let bulldog = Filter { $0.breed == .bulldog }
static let chowChow = Filter { $0.breed == .chowChow }
}

dog.filtering(.boxer).matching // Hugo
dog.filtering(.not(.husky, .chowChow)).rest // Rudolph, Sammuel
dog.filtering(Filter.boxer.or(.chowChow)).matching // Hugo, Sammuel

Conclusions

When developing code try to make it readable, maintainable, and easy to extend. Your code should read almost like a normal English sentence — that’s the difference between bad, good, and better code.

Better Programming

Advice for programmers.

Fernando Moya de Rivas

Written by

Enthusiast of Mobile development, interested in Art and Design.

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade