It’s brilliant when you can work with people smarter (or even a lot smarter) than you! You can learn so much from them. And then write about the things you’ve learned 🙃.

if and guard let

Have you ever been tired of repeating if let or guard let statements, to perform certain operations, that should be performed only if a value is not nil? From time to time your code could become like this if you wanted to call a function with unwrapped value:

if let unwrapped1 = optional1 {
	doTaskThatReturnsVoid(with: unwrapped1)
}
if let unwrapped2 = optional2 {
	doTaskThatReturnsVoid(with: unwrapped2)
}
//...
if let unwrappedX = optionalX {
	doTaskThatReturnsVoid(with: unwrappedX)
}

or like this:

struct Bar {
	let name: String
	let error: Error?
}

func foo(with bars: [Bar]) {
	var errors: [Error] = []
	bars.forEach {
		guard let error = $0.error else { return }
		errors.append(error)
	}
	process(errors)
}

Sometimes, it’s too much typing, isn’t it? But there’s an alternative …

flatMapping from T→Void

Optionals are monads, on which you can call flatMap function, that unwraps an optional (of hypothetical type A, a.k.a Optional<A>) if it contains a value, applies to it a transform, that converts a value from type A to type B and wraps it back into an optional of type B (a.k.a. Optional<B>). PS. if you want to understand functors, applicatives and monads I strongly recommend this video.

So, if we have an optional, we can call flatMap on it. We have to pass a closure transforming from A→B to the function. What if we wanted to transform from T→Void?

It would work. According to Swift compiler, transformation from T to nothing (a.k.a. Void) is a valid transformation. So let’s use that fact!

let optional1 = getOptional1()
optional1.flatMap { self.doTaskThatReturnsVoid(with: $0) }

Note that we don’t have to use a capture list (see more on them here) since flatMap takes a non-escaping closure as an argument. In my opinion the real beauty of the solution meets the eye here:

func foo(with bars: [Bar]) {
	var errors: [Error] = []
	bars.forEach {
		$0.error.flatMap { errors.append($0) }
	}
	process(errors)
}

Neat! We can append error to the array, only if it contains a value. We got rid of guard let similarly to if let in the previous example.

Drawbacks

Seeing code with transforms from T→Void in flatMap might seem a bit odd at first glance. It was odd for me. But once I’ve seen it used multiple times in the code that would require a few if lets in a row I started appreciating the solution. Check it out, maybe you will start using it too! :)

Final words

Big thanks to people I work with for the things I’ve been learning from you ❤️!

References

Update 4.12.2016 21:30

We’ve received a tip and correction from Ole Begemann. Thank you Ole ❤️🙃!

There’s a mistake in flatMapping from T→Void paragraph. It actually describes map instead of flatMap function. The map function takes a transform closure from T→U, whereas flatMap from T→U?. You can see declarations below:

public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?

So actually map and flatMap would have the same effect in our scenario. You can check this snippet in a Playground to see how it works!

import Foundation

enum Error: Swift.Error {
    case small
    case big
}

struct Bar {
    let name: String
    let error: Error?
}

let bars = [
    Bar(name: "1", error: Error.small),
    Bar(name: "2", error: nil),
    Bar(name: "3", error: Error.big),
]

func fooFlatMap(with bars: [Bar]) {
    var errors: [Error] = []
    bars.forEach {
        $0.error.flatMap { errors.append($0) }
    }
    print("\(errors)")
}

func fooMap(with bars: [Bar]) {
    var errors: [Error] = []
    bars.forEach {
        $0.error.map { errors.append($0) }
    }
    print("\(errors)")
}

fooFlatMap(with: bars)
fooMap(with: bars)

Update 5.12.2016 9:30

@rosskimes had a good thought - forEach in the example can actually be replaced by let errors: [Error] = bars.flatMap { $0.error }. So, let us show you a more convincing example.

In snippet below CoreDataWorker mentioned in issue #28 is used to remove all entities from the database. A dispatchGroup synchronizes delete operations. All potential errors are appended to an array and processed when all operations are finished.

func removeData() {
	let dispatchGroup = DispatchGroup()
	
	var errors: [Error] = []
	
	let completion = { (error: Error?) in
	    error.flatMap { errors.append($0) }
	    dispatchGroup.leave()
	}
	
	dispatchGroup.enter()
	coreDataWorker.removeAllEntitiesOfType(Entity1.self, completion: completion)
	
	dispatchGroup.enter()
	coreDataWorker.removeAllEntitiesOfType(Entity2.self, completion: completion)
	//...
	
	dispatchGroup.notify(queue: DispatchQueue.main) {
	    guard errors.isEmpty else { process(errors); return }
	    //success, proceed with further operations
	}
}