EP 44 · The Many Faces of Flat‑Map · Jan 21, 2019 ·Members

Video #44: The Many Faces of Flat‑Map: Part 3

smart_display

Loading stream…

Video #44: The Many Faces of Flat‑Map: Part 3

Episode: Video #44 Date: Jan 21, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep44-the-many-faces-of-flat-map-part-3

Episode thumbnail

Description

We are now ready to answer the all-important question: what’s the point? We will describe 3 important ideas that are now more accessible due to our deep study of map, zip and flatMap. We will start by showing that this trio of operations forms a kind of functional, domain-specific language for data transformations.

Video

Cloudflare Stream video ID: e96952083ac882a3f9f603233dafd438 Local file: video_44_the-many-faces-of-flat-map-part-3.mp4 *(download with --video 44)*

References

Transcript

0:05

It’s time to ask “what’s the point?” and “what’s the point is three-fold. We are going to discuss three important aspects in having a deep understanding of flatMap :

0:17

We are going to show that map , zip and flatMap form a kind of functional, domain-specific language for modeling pipelines of data. And seeing a usage of one of these operators we will already have a strong intuition for what that line of code could possibly be doing, without even knowing too much about the types involved. Once we see that we will have convinced ourselves that these operations have a very well-defined signature, and we shouldn’t be smudging it in order to suite our needs. Sometimes we’ll have a function signature that looks kinda like flatMap but it isn’t quite and we’ll want to call it flatMap for ease. We want to make the case that everything we have learned shows us that is the wrong decision, and our APIs will be better off if we do not do that. And then finally, we begin to ask ourselves very complex questions about how all of these operators interact with each other. These are problems that we feel would have been intractable if we didn’t already have a solid foundation of understand the purpose of map , zip and flatMap , and we would have had a very difficult time coming up with any concise, clear answers.

1:36

The meanings behind the signatures of map , zip and flatMap are very concise, and describe very well what their purpose is. This intuition is so strong that we can be presented with a whole new type that we are not familiar with and be told it has a map operation and instantly know that means we can unwrap the type, apply a transformation and wrap it back up. Or be told that it has a zip operation and so that we can take a bunch of values, independently run their computations, and obtain a new, single value from the type. Or be told that it has a flatMap operation, and now you know you can can sequence these values together so that one follows another.

2:20

So, let’s look at what power this gives us by looking at some interesting uses of these operators on some concrete types: Imperative nil handling

2:31

Suppose we have a JSON file sitting in our resources directory that has some data for a user: { "email": " [email protected] ", "id": 42, "name": "Blob" }

2:41

And we have the corresponding Codable model: struct User: Codable { let email: String let id: Int let name: String }

2:47

Now say we want to load this JSON payload and instantiate a user from decoding it. Let’s start with a naive, imperative approach.

3:03

We first need to get access to the path to the resource file: Bundle.main.path(forResource: "user", ofType: "json") // "…/user.json"

3:36

Now technically this is an optional string, so we need to unwrap it somehow. We could force unwrap it if we don’t care about crashing, or we could use if let if we want to use some of Swift’s simpler constructs: if let path = Bundle.main.path(forResource: "user", ofType: "json") { }

4:00

In order to load the data at this path we need an

URL 4:10

Notice that this initializer returns an honest

URL 4:15

Next we need to load the data, and we can do that with a Data initializer: if let path = Bundle.main.path(forResource: "user", ofType: "json") { let url = URL(fileURLWithPath: path) let data = Data(contentsOf: url) Call can throw but is not marked with ‘try’

URL 4:26

This initializer can throw, but let’s assume for a moment that we do not care about error handling, we just want to know if it succeeds or fails. Then we can optionally try it: if let path = Bundle.main.path(forResource: "user", ofType: "json") { let url = URL(fileURLWithPath: path) let data = try? Data(contentsOf: url) if let data = data { } }

URL 4:49

And then finally we need to decode this data into our model: if let path = Bundle.main.path(forResource: "user", ofType: "json") { let url = URL(fileURLWithPath: path) if let data = try? Data(contentsOf: url) { JSONDecoder().decode(User.self, from: data) Call can throw but is not marked with ‘try’

URL 5:01

Again this can throw but currently we do not care about the error messaging, just the success and failure: if let path = Bundle.main.path(forResource: "user", ofType: "json") { let url = URL(fileURLWithPath: path) if let data = try? Data(contentsOf: url) { try? JSONDecoder().decode(User.self, from: data) } }

URL 5:07

Now, in order to capture the resulting user for this we need to have a variable on the outside of the if ready to assign: let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json") { let url = URL(fileURLWithPath: path) if let data = try? Data(contentsOf: url) { user = try? JSONDecoder().decode(User.self, from: data) } else { user = nil } } else { user = nil }

URL 5:43

Now, we would never write Swift code like this because Swift gives us a tool to simplify: we can do multiple if let bindings in a single line: let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json"), let data = try? Data(contentsOf: url) { Use of unresolved identifier ‘url’

URL 6:05

We can’t break it out onto its own line because the value isn’t optional: let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json"), let url = URL(fileURLWithPath: path), let data = try? Data(contentsOf: url) { Initializer for conditional binding must have Optional type, not ‘URL’

URL 6:32

You could also wrap the value in an optional just so that you get access to if let : let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json"), let url = Optional(URL(fileURLWithPath: path)), let data = try? Data(contentsOf: url) { user = try? JSONDecoder().decode(User.self, from: data) } else { user = nil } But it’s pretty weird to wrap something in an optional just to unwrap it immediately, all with the intent of extracting a unit of work.

URL 6:46

Another weird thing we could do is use case let to work around this: let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json"), case let url = URL(fileURLWithPath: path), let data = try? Data(contentsOf: url) { user = try? JSONDecoder().decode(User.self, from: data) } else { user = nil } Because pattern matching is allowed here, it can be used as a catch-all.

URL 7:03

Both of these methods are pretty awkward and neither is better than the other, but it lets us clean up the rest of our code. let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json"), case let url = URL(fileURLWithPath: path), let data = try? Data(contentsOf: url) { user = try? JSONDecoder().decode(User.self, from: data) } else { user = nil }

URL 7:24

We’ve accomplished what we set out to do, but there are a few downsides. For one, we’ve given explicit names ( path , url , data ) for all of these units of work even though that are pretty temporary and don’t seem important to what we are trying to accomplish. Also unfortunate that we need to assign the user to nil in the failure branches, but at least the compiler has our back on that. And at least we were able to reduce the nesting one level. Correction We commented the user = nil branch out and noted that things still compiled, but the playground hides a very helpful warning: “immutable value ‘user’ was never used; consider removing it”. If we had tried to use our user later on, we would have gotten an error: “constant ‘user’ used before being initialized”. This means the compiler would have required that extra branch where we explicitly assigned nil after all. Nil handling pipelines

URL 8:19

But, let’s now refactor using our knowledge of flatMap .

URL 8:27

We start with the optional resource path: Bundle.main.path(forResource: "user", ofType: "json") // String? We want to turn this into a

URL 9:10

We can even do it point-free by getting rid of the reference to $0 and just passing along the initializing function directly: Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) // URL?

URL 9:34

Next we need to load the data at this URL, and that is a failable operation since the initializer throws. This means we need flatMap so that we can chain the failable operation we currently have (resource path) onto the next failable operation (data loading): Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } // Data?

URL 10:05

Then we need to decode this data into the User model, which could also fail. Sounds like another use of flatMap : Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode(User.self, from: $0) } // User?

URL 10:27

And finally we can assign it to a variable: let newUser = Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode(User.self, from: $0) } // User?

URL 10:30

Here in a single expression we have really packed a punch. Each line is understandable in isolation because the operator makes it clear whether we are doing a pure transformation ( map ) or a failable transformation ( flatMap ), and the work being done in each of those operators has no dependency on the outside world and so nothing strange can happen.

URL 10:56

Let’s kick it up a notch. There’s another JSON file in our resources directory that has an array of invoices for this user. [ { "amountPaid": 1000, "amountDue": 0, "closed": true, "id": 1 }, { "amountPaid": 500, "amountDue": 500, "closed": false, "id": 2 } ]

URL 11:04

We can load this data just like we did for the user. We can define a model struct. struct Invoice: Codable { let amountDue: Int let amountPaid: Int let closed: Bool let id: Int }

URL 11:08

And to decode that JSON into an array of invoices, we can copy our earlier work and update the resource path and decoding logic. let invoices = Bundle.main.path( forResource: "invoices", ofType: "json" ) .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } // [Invoice]?

URL 11:40

But now say we want both the user and their invoices together when they are both non- nil . Well we could of course do an if let : if let newUser = newUser, let invoices = invoices { }

URL 11:57

But we are again in a world where we need a variable on the outside of the if to capture the work being done inside the if . So instead, let’s use zip on optionals, which is kind of a functional version of if – let . func zip<A, B>(_ a: A?, _ b: B?) -> (A, B)? { if let a = a, let b = b { return (a, b) } return nil }

URL 12:27

And now we can zip our optional user and optional invoices together. zip(newUser, invoices) // (User, [Invoice])?

URL 12:36

Even better, there’s no reason to assign the user and invoices to arrays anymore, let’s just inline all of that work! zip( Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode(User.self, from: $0) }, Bundle.main.path(forResource: "invoices", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } ) // (User, [Invoice])?

URL 12:54

And say we didn’t want want to deal with tuples, but wanted a proper type to hold this data. For example: struct UserEnvelope { let user: User let invoices: [Invoice] }

URL 13:08

And then we can use zip(with:) that, given a transform function, glues an A and a B into a C : func zip<A, B, C>(with f: @escaping (A, B) -> C) -> (A?, B?) -> C? { return { zip($0, $1).map(f) } }

URL 13:27

To use this, we swap out zip for zip(with:) and hand it our initializer. zip(with: UserEnvelope.init)( Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode(User.self, from: $0) }, Bundle.main.path(forResource: "invoices", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } ) // UserEnvelope?

URL 13:39

And again, this single expression is packing a huge punch.

URL 13:43

When we see zip we know that we are combining multiple independent contextual values into a single one: in this case, a user loaded from JSON on disk and an array of invoices loaded from disk. Neither of these values depend on each other.

URL 14:11

When we see map we know we are performing a pure, infallible transformation on the underlying value inside the context.

URL 14:31

And when we see flatMap we know we are performing a failable transformation on that value in the context. This computation is dependent on the computations that came before it, whereas the zip is not.

URL 14:46

Now we’re beginning to see how these three operations are kind of forming a mini functional domain-specific language for transforming data. When we see one of these operations we know what it’s doing because each operation is doing something very specific and does it very well. Imperative error handling

URL 15:12

The work we did certainly packed a punch and celebrated the use of all three operations, map , zip , and flatMap , but because we were working within the world of optionals, we threw away a lot of information as to what might fail. In fact, we’re doing so much in this expression that a lot could fail. Let’s try refactoring things to use Swift error handling instead.

URL 15:41

Let’s approach the problem as we did last time, with a naive approach using Swift error handling. We’ll start with our old imperative code that worked with optionals. let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json"), case let url = URL(fileURLWithPath: path), let data = try? Data(contentsOf: url) { user = try? JSONDecoder().decode(User.self, from: data) } else { user = nil }

URL 16:02

First, let’s wrap the entire block of code in a do block, which lets us scope our error handling. do { let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json"), case let url = URL(fileURLWithPath: path), let data = try? Data(contentsOf: url) { user = try? JSONDecoder().decode(User.self, from: data) } else { user = nil } } catch { }

URL 16:11

This allows us to change our try? s into try s that may throw. do { let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json"), case let url = URL(fileURLWithPath: path), let data = try Data(contentsOf: url) { user = try JSONDecoder().decode(User.self, from: data) } else { user = nil } } catch { } Initializer for conditional binding must have Optional type, not ‘Data’

URL 16:27

Data is no longer optional, so we can remove it from our if – let chain and move it into the block. do { let user: User? if let path = Bundle.main.path(forResource: "user", ofType: "json"), case let url = URL(fileURLWithPath: path) { let data = try Data(contentsOf: url) user = try JSONDecoder().decode(User.self, from: data) } else { user = nil } } catch { }

URL 16:31

Now we’re in a world with two methods of error handling: optionals and throws. Just because the path is optional, our user must be as well.

URL 16:45

It would be easier to fail consistently with throwing errors, so let’s write a helper that either unwraps an optional or throws an error. func requireSome<A>(_ a: A?) throws -> A { guard let a = a else { throw } }

URL 17:26

What can we throw inside this function? Let’s define an error for the case where the expected value is absent. struct SomeExpected: Error {} Now we can throw this error or return an unwrapped value. func requireSome<A>(_ a: A?) throws -> A { guard let a = a else { throw SomeExpected() } return a }

URL 17:48

When we use our helper, we now have an unwrapped path to work with, and we can assign our non-optional user at the very end of this code block. do { let path = try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) let url = URL(fileURLWithPath: path) let data = try Data(contentsOf: url) let user = try JSONDecoder().decode(User.self, from: data) } catch { }

URL 18:12

Now we’ve fully migrated over to a completely different style of error handling, and it’s kind of amazing how much the shape has changed. We got rid of our earlier if – let nesting and chaining, and we’re left with an imperative, line-by-line assignment where failable operations are marked with try .

URL 18:30

Of course, we mentioned earlier that these assignments of temporary variables are kind of a bummer, so maybe we want to refactor them away. do { let user = try JSONDecoder().decode( User.self, from: try Data( contentsOf: URL( fileURLWithPath: try requireSome( Bundle.main.path( forResource: "user", ofType: "json" ) ) ) ) ) } catch { }

URL 19:14

We’ve traded out our temporary variables for a bit of a pyramid of doom. We might simplify a bit by removing some extra try s, since only one is required per expression. do { let user = try JSONDecoder().decode( User.self, from: Data( contentsOf: URL( fileURLWithPath: requireSome( Bundle.main.path( forResource: "user", ofType: "json" ) ) ) ) ) } catch { }

URL 19:25

But maybe this isn’t such a good idea, since it’s not longer clear which of these operations is failable. Error handling pipelines

URL 19:35

Let’s try something else. We’ve seen that we can also use the Result type for error handling, and that flatMap is perfect for chaining failable work along. So let’s refactor the above to use Result instead of throws .

URL 19:54

First we need to define a convenience initializer on Result to play nicely with throwing functions. This is a helper that will be included in the standard library when Swift 5 is released. extension Result where E == Swift.Error { init(catching f: () throws -> A) { do { self = .success(try f()) } catch { self = .failure(error) } } }

URL 20:26

With this in place, we can start modifying the pipeline by wrapping each optional in our helper. do { let path = Result { try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } let url = URL(fileURLWithPath: path) Cannot convert value of type ‘Result<String, Swift.Error>’ to expected argument type ‘String’

URL 20:37

Now path wraps a result, so we can’t pass it to the

URL 21:01

The next two lines are failable, so we need to wrap them using our Result helper and use flatMap to chain them along. do { let path = Result { try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } let url = path.map(URL.init(fileURLWithPath:)) let data = url.flatMap { url in Result { try Data(contentsOf: url) } } let user = data.flatMap { data in Result { try JSONDecoder().decode(User.self, from: data) } } } catch { } ‘catch’ block is unreachable because no errors are thrown in ‘do’ block

URL 21:31

Now that we’re fully living in the world of results, we can get rid of the do block entirely. let path = Result { try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } let url = path.map(URL.init(fileURLWithPath:)) let data = url.flatMap { url in Result { try Data(contentsOf: url) } } let userResult = data.flatMap { data in Result { try JSONDecoder().decode(User.self, from: data) } }

URL 21:40

And there’s no need to declare all of these variables. We can chain each expression along. let userResult = Result { try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Result { try Data(contentsOf: url) } } .flatMap { data in Result { try JSONDecoder().decode(User.self, from: data) } // Result<User, Swift.Error>

URL 22:03

Alright, let’s try adding those extra units of work from before, like loading some invoices and bundling the user and invoices up in another type. First, the invoices can be loaded, as before, by copying this work and making some changes. let invoicesResult = Result { try requireSome( Bundle.main.path(forResource: "invoices", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Result { try Data(contentsOf: url) } } .flatMap { data in Result { try JSONDecoder().decode([Invoice].self, from: data) } // Result<[Invoice], Swift.Error>

URL 22:30

Now let’s bundle them up together. We can use zip , which we’ve defined before : func zip<A, B, E>( _ a: Result<A, E>, _ b: Result<B, E> ) -> Result<(A, B), E> { switch (a, b) { case let (.success(a), .success(b)): return .success((a, b)) case let (.failure(e), _): return .failure(e) case let (.success, .failure(e)): return .failure(e) } }

URL 22:44

And we can get rid of our userResult and invoicesResult variable names. zip( Result { try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Result { try Data.init(contentsOf: url) } } .flatMap { data in Result { try JSONDecoder().decode(User.self, from: data) } }, Result { try requireSome( Bundle.main.path(forResource: "invoices", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Result { try Data.init(contentsOf: url) } } .flatMap { data in Result { try JSONDecoder().decode([Invoice].self, from: data) } } ) // Result<(User, [Invoice]), Swift.Error>

URL 22:55

We can even define zip(with:) on Result , which includes that transform function that allows us to combine two results. func zip<A, B, C, E>( with f: @escaping (A, B) -> C ) -> (Result<A, E>, Result<B, E>) -> Result<C, E> { return { zip($0, $1).map(f) } }

URL 23:01

And we can pass the UserEnvelope initializer as this transform function. zip(with: UserEnvelope.init)( Result { try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Result { try Data.init(contentsOf: url) } } .flatMap { data in Result { try JSONDecoder().decode(User.self, from: data) } }, Result { try requireSome( Bundle.main.path(forResource: "invoices", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Result { try Data.init(contentsOf: url) } } .flatMap { data in Result { try JSONDecoder().decode([Invoice].self, from: data) } } ) // Result<UserEnvelope, Swift.Error>

URL 23:08

Let’s compare this work with the Optional version from earlier, side-by-side: zip(with: UserEnvelope.init)( Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode(User.self, from: $0) }, Bundle.main.path(forResource: "invoices", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } ) zip(with: UserEnvelope.init)( Result { try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Result { try Data.init(contentsOf: url) } } .flatMap { data in Result { try JSONDecoder().decode(User.self, from: data) } }, Result { try requireSome( Bundle.main.path(forResource: "invoices", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Result { try Data.init(contentsOf: url) } } .flatMap { data in Result { try JSONDecoder().decode([Invoice].self, from: data) } } )

URL 23:20

Here we have two units of work that do very similar things and even have very similar shapes! They only differ in how they handle errors. The optional version discards them entirely, while the result version captures the first thing that fails.

URL 23:39

What I like about the shape being so similar is that this was not the case when it came to Swift’s more imperative approaches: the throw ing version looked nothing like the if – let version. Validation pipelines

URL 24:08

Now that we’ve seen that this works with optionals and results, let’s try Validated .

URL 24:15

The first thing we need to do is make sure Validated plays nicely with throwing functions, so we’ll need an initializer like the one on Result . We can copy-paste it and make minimal changes. extension Validated where E == Swift.Error { init(catching f: () throws -> A) { do { self = .valid(try f()) } catch { self = .invalid(NonEmptyArray(error)) } } }

URL 24:40

We also need our zip s defined, so let’s paste them in. func zip<A, B, E>( _ a: Validated<A, E>, _ b: Validated<B, E> ) -> Validated<(A, B), E> { switch (a, b) { case let (.valid(a), .valid(b)): return .valid((a, b)) case let (.valid, .invalid(e)): return .invalid(e) case let (.invalid(e), .valid): return .invalid(e) case let (.invalid(e1), .invalid(e2)): return .invalid(e1 + e2) } } func zip<A, B, C, E>( with f: @escaping (A, B) -> C ) -> (Validated<A, E>, Validated<B, E>) -> Validated<C, E> { return { zip($0, $1).map(f) } } It’s interesting to note that the zip on Validated has some super powers: rather than bail out on the first error, it combines errors using its non-empty array.

URL 24:59

And now, all we need to do is swap Result out for Validated . zip(with: UserEnvelope.init)( Validated { try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Validated { try Data(contentsOf: url) } } .flatMap { data in Validated { try JSONDecoder().decode(User.self, from: data) } }, Validated { try requireSome( Bundle.main.path(forResource: "invoices", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Validated { try Data.init(contentsOf: url) } } .flatMap { data in Validated { try JSONDecoder().decode([Invoice].self, from: data) } } )

URL 25:12

And it works. We’ve taken a unit of work that worked with optionals and results and teleported it to work with validated values using the same functional operations.

URL 25:38

And we have the added benefit that if something goes wrong with both the user and invoices we’ll get both error notifications.

URL 25:37

For instance, we can change our JSON payloads to be invalid, and we get two failures, whereas with Result we only get the first.

URL 26:23

It’s really cool that we’ve taken a single unit of work that can fail in some way and show that it works with optionals, results, and validated values. Each case looked almost identical because they all used the same functional domain specific language of map , zip , and flatMap . Lazy pipelines

URL 26:55

And this mechanism doesn’t only work with error handling. It holds true for any type that speaks the functional domain specific language of map , zip , and flatMap for data transformation pipelines. Let’s see how our code can be changed to work with Func , which has no concept of failure.

URL 27:34

In order to derive our functional pipeline for Func , we can copy over our work on Result and make some very small changes. First, we can replace Result with Func . Func { try requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Func { try Data(contentsOf: url) } } .flatMap { data in Func { try JSONDecoder().decode(User.self, from: data) } }

URL 27:47

Let’s not worry about error handling and instead focus on the lazy aspect of Func . So we can change all of our try s to try! and assume no failure. Func { try! requireSome( Bundle.main.path(forResource: "user", ofType: "json") ) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Func { try! Data(contentsOf: url) } } .flatMap { data in Func { try! JSONDecoder().decode(User.self, from: data) } }

URL 28:01

We also might as well simplify by force-unwrapping the path rather than use requireSome . Func { Bundle.main.path(forResource: "user", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Func { try! Data(contentsOf: url) } } .flatMap { data in Func { try! JSONDecoder().decode(User.self, from: data) } } // Func<Void, User>

URL 28:12

This all works and now we have a lazy value of a User . The creation of this value has done none of the work inside. We’ve merely composed the work together using map and flatMap . Work only happens the moment we call run . let lazyUser = Func { Bundle.main.path(forResource: "user", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Func { try! Data(contentsOf: url) } } .flatMap { data in Func { try! JSONDecoder().decode(User.self, from: data) } } lazyUser.run(()) // User

URL 28:38

Func also has zip , which allows us to combine lazy values together: func zip<A, B, C>( _ ab: Func<A, B>, _ ac: Func<A, C> ) -> Func<A, (B, C)> { return Func<A, (B, C)> { a in (ab.run(a), ac.run(a)) } } func zip<A, B, C, D>( with f: @escaping (B, C) -> D ) -> (Func<A, B>, Func<A, C>) -> Func<A, D> { return { zip($0, $1).map(f) } }

URL 28:53

So we could take a lazy user and zip it with a lazy array of invoices. zip( Func { Bundle.main.path(forResource: "user", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Func { try! Data(contentsOf: url) } } .flatMap { data in Func { try! JSONDecoder().decode(User.self, from: data) } }, Func { Bundle.main.path(forResource: "invoices", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Func { try! Data(contentsOf: url) } } .flatMap { data in Func { try! JSONDecoder().decode([Invoice].self, from: data) } } ) // Func<Void, (User, [Invoice])>

URL 29:10

And we can use zip(with:) the UserEnvelope initializer to further transform our lazy value. zip(with: UserEnvelope.init)( Func { Bundle.main.path(forResource: "user", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Func { try! Data(contentsOf: url) } } .flatMap { data in Func { try! JSONDecoder().decode(User.self, from: data) } }, Func { Bundle.main.path(forResource: "invoices", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Func { try! Data(contentsOf: url) } } .flatMap { data in Func { try! JSONDecoder().decode([Invoice].self, from: data) } } ) // Func<Void, UserEnvelope>

URL 29:20

And here, map , zip , and flatMap mean pretty much the same thing as before, but specific to lazy values: map means to unwrap a lazy value, transform it, then rewrap it up in another lazy value. zip means to combine lazy values into a single lazy value. flatMap means to sequence. Once we have a lazy value, we can use flatMap to sequence it into another lazy computation. Asynchronous pipelines

URL 29:58

Alright, if we can do this work with Func , we should be able to do it with Parallel . Let’s again, do it one step at a time. First, we’ll copy and paste loading and decoding a User and make some small changes to work with parallel values. Parallel { callback in callback(Bundle.main.path(forResource: "user", ofType: "json")!) } .map(URL.init(fileURLWithPath:)) .flatMap { url in Parallel { callback in callback(try! Data(contentsOf: url)) } } .flatMap { data in Parallel { callback in callback(try! JSONDecoder().decode(User.self, from: data)) } } // Parallel<User> Rather than return the values directly, we pass them to Parallel ’s callback function.

URL 30:37

And with that, we have a parallel value of a user. We built it from things that are synchronous, so it doesn’t feel very parallel right now. We can, however, define a convenience initializer that dispatches its work on a background queue. extension Parallel { init(_ work: @escaping () -> A) { self = Parallel { callback in DispatchQueue.global().async { callback(work()) } } } }

URL 31:49

Now we can clean things up by doing work directly. Parallel { Bundle.main.path(forResource: "user", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Parallel { try! Data(contentsOf: url) } } .flatMap { data in Parallel { try! JSONDecoder().decode(User.self, from: data) } } // Parallel<User>

URL 32:01

We can even make this a bit nicer using Swift’s auto-closure mechanism, which automatically wraps a value in a closure that can be run later. extension Parallel { init(_ work: @autoclosure @escaping () -> A) { self = Parallel { callback in DispatchQueue.global().async { callback(work()) } } } }

URL 32:12

And now we can pass the values directly to our convenience initializer. Parallel(Bundle.main.path(forResource: "user", ofType: "json")!) .map(URL.init(fileURLWithPath:)) .flatMap { url in Parallel(try! Data(contentsOf: url)) } .flatMap { data in Parallel(try! JSONDecoder().decode(User.self, from: data)) } // Parallel<User>

URL 32:36

Now we have a nice way of creating parallel values that run on a background queue so that we don’t block the main queue if we started loading JSON off the network instead, or if we were decoding data that took awhile.

URL 33:30

Can we do all the zipping we did before? We can! Here are the zip s that we previously defined on Parallel . func zip<A, B>( _ pa: Parallel<A>, _ pb: Parallel<B> ) -> Parallel<(A, B)> { return Parallel<(A, B)> { callback in var optionalA: A? var optionalB: B? pa.run { a in optionalA = a if let b = optionalB { callback((a, b)) } } pb.run { b in optionalB = b if let a = optionalA { callback((a, b)) } } } } func zip<A, B, C>( with f: @escaping (A, B) -> C ) -> (Parallel<A>, Parallel<B>) -> Parallel<C> { return { zip($0, $1).map(f) } } What’s special about zip on Parallel is that it allows you to run two Parallel values in parallel and collect the results in the end. Our version isn’t the thread-safe version, it’s just the simplest version possible to demonstrate what zip can do on this structure.

URL 34:02

Now we can zip this parallel unit of work of loading a user with another parallel unit of work of loading invoices. zip( Parallel { Bundle.main.path(forResource: "user", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Parallel { try! Data(contentsOf: url) } } .flatMap { data in Parallel { try! JSONDecoder().decode(User.self, from: data) } }, Parallel { Bundle.main.path(forResource: "invoices", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Parallel { try! Data(contentsOf: url) } } .flatMap { data in Parallel { try! JSONDecoder().decode([Invoice].self, from: data) } } ) // Parallel<(User, [Invoice])>

URL 34:22

If we can zip our data into a tuple, we can zip it into its own structure using zip(with:) . zip(with: UserEnvelope.init)( Parallel { Bundle.main.path(forResource: "user", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Parallel { try! Data(contentsOf: url) } } .flatMap { data in Parallel { try! JSONDecoder().decode(User.self, from: data) } }, Parallel { Bundle.main.path(forResource: "invoices", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Parallel { try! Data(contentsOf: url) } } .flatMap { data in Parallel { try! JSONDecoder().decode([Invoice].self, from: data) } } ) // Parallel<UserEnvelope>

URL 34:44

In order to get to the value in our Parallel , we need to run it. zip(with: UserEnvelope.init)( Parallel { Bundle.main.path(forResource: "user", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Parallel { try! Data(contentsOf: url) } } .flatMap { data in Parallel { try! JSONDecoder().decode(User.self, from: data) } }, Parallel { Bundle.main.path(forResource: "invoices", ofType: "json")! } .map(URL.init(fileURLWithPath:)) .flatMap { url in Parallel { try! Data(contentsOf: url) } } .flatMap { data in Parallel { try! JSONDecoder().decode([Invoice].self, from: data) } } ) .run { env in print(env) }

URL 34:58

This time, both operations ran on a background queue, unlike Func , which ran each lazy value one after the other.

URL 35:11

These parallels may not be doing a ton of work right now, but if we were loading data over the network or decoding took some time, it would prevent us from blocking the main queue. Till next time

URL 35:33

Now we see map , zip and flatMap are an important trio of operations and each does one thing and does it well. We should now be able to convince ourselves that we shouldn’t be smudging their definitions just to suit our needs. We will likely come across functions with signatures that look a lot like flatMap and may even be tempted to call it flatMap , but doing so can destroy all of our intuitions around what flatMap is. Right now we have 6 types that we are very familiar with and all of the operations behave roughly the same, even for very different types.

URL 36:26

And this lesson is an important one, but just 10 or 11 months ago we had a decisive moment in Swift history where the community got to learn from this and put it to real-world use. We actually had an entire episode dedicated to this back then, but now we are even in a better position to appreciate it, so let’s briefly recall the problem. References SE-0235: Add Result to the Standard Library Nov 7, 2018 The Swift evolution review of the proposal to add a Result type to the standard library. It discussed many functional facets of the Result type, including which operators to include (including map and flatMap ), and how they should be defined. https://forums.swift.org/t/se-0235-add-result-to-the-standard-library/17752 Railway Oriented Programming — error handling in functional languages Scott Wlaschin • Jun 4, 2014 This talk explains a nice metaphor to understand how flatMap unlocks stateless error handling. Note When you build real world applications, you are not always on the “happy path”. You must deal with validation, logging, network and service errors, and other annoyances. How do you manage all this within a functional paradigm, when you can’t use exceptions, or do early returns, and when you have no stateful data? This talk will demonstrate a common approach to this challenge, using a fun and easy-to-understand “railway oriented programming” analogy. You’ll come away with insight into a powerful technique that handles errors in an elegant way using a simple, self-documenting design. https://vimeo.com/97344498 A Tale of Two Flat‑Maps Brandon Williams & Stephen Celis • Mar 27, 2018 Up until Swift 4.1 there was an additional flatMap on sequences that we did not consider in this episode, but that’s because it doesn’t act quite like the normal flatMap . Swift ended up deprecating the overload, and we discuss why this happened in a previous episode: Note Swift 4.1 deprecated and renamed a particular overload of flatMap . What made this flatMap different from the others? We’ll explore this and how understanding that difference helps us explore generalizations of the operation to other structures and derive new, useful code! https://www.pointfree.co/episodes/ep10-a-tale-of-two-flat-maps Downloads Sample code 0044-the-many-faces-of-flatmap-pt3 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .