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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if let unwrapped1 = optional1 { doTaskThatReturnsVoid(with: unwrapped1) } if let unwrapped2 = optional2 { doTaskThatReturnsVoid(with: unwrapped2) } //... if let unwrappedX = optionalX { doTaskThatReturnsVoid(with: unwrappedX) } |
or like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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!
1 2 3 4 5 |
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:
1 2 3 4 5 6 7 8 9 10 |
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:
1 2 3 4 5 |
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!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 } } |
Pingback: Stylishly Swifty – Under The Bridge