EP 24 · The Many Faces of Zip · Jul 30, 2018 ·Members

Video #24: The Many Faces of Zip: Part 2

smart_display

Loading stream…

Video #24: The Many Faces of Zip: Part 2

Episode: Video #24 Date: Jul 30, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep24-the-many-faces-of-zip-part-2

Episode thumbnail

Description

In part two of our series on zip we will show that many types support a zip-like operation, and some even support multiple distinct implementations. However, not all zips are created equal, and understanding this can lead to some illuminating properties of our types.

Video

Cloudflare Stream video ID: 806e21617f968a072fbd9c5edbc2ece6 Local file: video_24_the-many-faces-of-zip-part-2.mp4 *(download with --video 24)*

References

Transcript

0:05

In the last episode we began the journey into exploring the “many faces of zip”. First we played around with the zip that the standard library gives us a bit and saw that it was really useful for coordinating the elements of two sequences in a very safe way so that we don’t have to juggle indices.

0:27

Then we zoomed out a bit and looked at the true signature of zip , and we saw that what it was really doing was kinda flipping containers: it transformed a tuple of arrays into an array of tuples.

0:45

After that we saw that zip is just a generalization of map : where map allows us to transform a function (A) -> B into a function ([A]) -> [B] , zip allowed us to transform a function (A, B) -> C into a function ([A], [B]) -> [C] .

1:14

This empowered us to define zip on optionals, which is not something that people typically do. It ended up being the perfect solution to a problem that plagued Swift 1: nested if let s! Swift 2 fixed this, but zip still can provide a more ergonomic solution than multiple if let s on the same line. Zip on other types: Result

1:38

In this episode we are going to continue defining zip on a bunch of types that we might not typically think should have a zip operation.

1:57

Let’s start by looking at the Result type.

2:05

Here’s all the work we did last time: func zip2<A, B>(_ xs: [A], _ ys: [B]) -> [(A, B)] { var result: [(A, B)] = [] (0..<min(xs.count, ys.count)).forEach { idx in result.append((xs[idx], ys[idx])) } return result } func zip3<A, B, C>(_ xs: [A], _ ys: [B], _ zs: [C]) -> [(A, B, C)] { return zip2(xs, zip2(ys, zs)) // [(A, (B, C))] .map { a, bc in (a, bc.0, bc.1) } } func zip2<A, B, C>( with f: @escaping (A, B) -> C ) -> ([A], [B]) -> [C] { return { zip2($0, $1).map(f) } } func zip3<A, B, C, D>( with f: @escaping (A, B, C) -> D ) -> ([A], [B], [C]) -> [D] { return { zip3($0, $1, $2).map(f) } } func zip2<A, B>(_ a: A?, _ b: B?) -> (A, B)? { guard let a = a, let b = b else { return nil } return (a, b) } func zip3<A, B, C>(_ a: A?, _ b: B?, _ c: C?) -> (A, B, C)? { return zip2(a, zip2(b, c)) .map { a, bc in (a, bc.0, bc.1) } } func zip2<A, B, C>( with f: @escaping (A, B) -> C ) -> (A?, B?) -> C? { return { zip2($0, $1).map(f) } } func zip3<A, B, C, D>( with f: @escaping (A, B, C) -> D ) -> (A?, B?, C?) -> D? { return { zip3($0, $1, $2).map(f) } } We defined a bunch of zip functions on arrays and optionals: zip2 and zip3 for zipping two and three values accordingly, and zip2(with:) and zip3(with:) , for mapping the resulting tuple into something new.

2:27

We’ve discussed the Result type in the past , and you may have even encountered it yourself. It’s a nice way of representing a value that can either hold a successful value or an error. enum Result<A, E> { case success(A) case failure(E) }

2:38

In our episode on map , we showed that Result supports a map operation that has a lot of the same characteristics that map on Array and Optional has.

2:48

We defined it like so: func map<A, B, E>( _ f: @escaping (A) -> B) -> (Result<A, E> ) -> Result<B, E> { return { result in switch result { case let .success(a): return .success(f(a)) case let .failure(e): return .failure(e) } } } This says that we can lift a function (A) -> B up to a function (Result<A, E>) -> Result<B, E> by just applying the function if the result is in the success state, and otherwise just passing the error on.

3:07

Because Result supports a map operation, does it also support zip ?

3:16

Let’s try to define it: func zip2<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 (.success, .failure(e)): return .failure(e) case let (.failure(e), .success): return .failure(e) case let (.failure(e1), .failure(e2)): <#???#> } }

5:13

Everything was going smoothly until we got to the case of both results being in the failed state. We could certainly return .failure(e1) , but then we are completely ignoring the error e2 . We could also return .failure(e2) , but then we are ignoring the error e1 . Both of these allow us to implement this function, but neither seem right. They throw away some really important information.

5:49

What we have discovered here is that Result seemingly supports two completely different zip -like operations, but neither one seems great since they throw away data. That’s kind of a bummer. Zip on other types: Validation

6:07

There’s an easy fix though. In our episode on the NonEmpty type we defined a variation on Result that doesn’t allow just any error, but forces us to use a non-empty array of errors: import NonEmpty enum Validated<A, E> { case valid(A) case invalid(NonEmptyArray<E>) } This type allows you to represent a value that has been validated. If it’s valid you get something in the .valid case, and otherwise you get a non-empty array of errors of what was wrong with the value.

6:53

The map on Validated looks pretty much exactly like it does for Result , so we can copy-paste and fix a few small things: func map<A, B, E>( _ f: @escaping (A) -> B ) -> (Validated<A, E>) -> Validated<B, E> { return { validated in switch validated { case let .valid(a): return .valid(f(a)) case let .invalid(e): return .invalid(e) } } }

7:26

Now, can we define zip on this type? Well, I’m going to start by copy-pasting what we have for Result and fixing a few things: func zip2<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 .failure(e1) return . invalid(e2) } }

7:56

This is compiling, but rather than discard any information, we can take advantage of the fact that our invalid cases are non-empty arrays and can be concatenated. return .invalid(e1 + e2)

8:17

Now we’re not throwing away any information! When we zip up two invalid values, we just combine the errors.

8:31

And we can of course pretty easily define a zip2(with:) in terms of zip2 : func zip2<A, B, C, E>( with f: @escaping (A, B) -> C ) -> (Validated<A, E>, Validated<B, E>) -> Validated<C, E> { return { a, b in zip2(a, b) |> map(f) } } And it’s mostly the same as our other zip(with:) functions. We just had to change the signature and, because we defined map as a free function, use |> .

9:13

And we can easily define a zip3 : func zip3<A, B, C, E>( _ a: Validated<A, E>, _ b: Validated<B, E>, _ c: Validated<C, E> ) -> Validated<(A, B, C), E> { return zip2(a, zip2(b, c)) |> map { ($0, $1.0, $1.1) } } func zip3<A, B, C, D, E>( with f: @escaping (A, B, C) -> D ) -> (Validated<A, E>, Validated<B, E>, Validated<C, E>) -> Validated<D, E> { return { zip3($0, $1, $2) |> map(f) } }

9:49

And these functions were mostly the same as our other versions of zip .

10:06

Let’s explore this zip with an example. Given a compute function that runs a computation. import Foundation func compute(_ a: Double, _ b: Double) -> Double { return sqrt(a) + sqrt(b) }

10:24

We can cook up a helper that validates Double s: func validate( _ a: Double, label: String ) -> Validated<Double, String> { return a < 0 ? .invalid(NonEmptyArray("\(label) must be non-negative")) : .valid(a) }

11:22

The classic way of using compute would be to pass it some values directly. compute(2, 3) // 3.146264369941973

11:36

What if we want to work with validated values? We can’t merely pass them through: compute(validate(2, label: "first"), validate(3, label: "second")) Note Cannot convert value of type ‘Validated<Double, String>’ to expected argument type ‘Double’

11:58

We need some way of lifting compute up to the world of validated values, and that’s precisely what zip2(with:) does. zip2(with: compute)( validate(2, label: "first"), validate(3, label: "second") ) // valid(3.146264369941973)

12:17

And we get a valid result.

12:27

If we provide invalid data, we get an error. zip2(with: compute)( validate(-1, label: "first"), validate(3, label: "second") ) // invalid("first must be non-negative."[]) And if we provide even more invalid data, we get even more errors. zip2(with: compute)( validate(-1, label: "first"), validate(-3, label: "second") ) // invalid("first must be non-negative."["second must be non-negative."])

12:45

This is packing a lot into just a few lines of code. There’s not even a clear alternative to write this kind of code in Swift like there was for optionals because Swift does not give us syntax for Validated like it does for Optional s with if let . Without zip2 we would be forced to switch on a pair of validated values and handle all 4 cases each time, which would be a pain.

13:11

It is very interesting that Result did not have a clear zip -like function yet somehow Validated did. This is the beginnings of a very deep idea that we are only scratching at right now. Zip on other types: Func

13:37

Let’s try defining zip on one of the other types we talked about in our episode on map . We defined a type called F2 which we later gave a proper name Func . struct Func<R, A> { let apply: (R) -> A } Here it is in all its glory: it’s a type that merely wraps a function and is generic over its input and output.

14:17

We were able to define map on this quite easily: func map<A, B, R>(_ f: @escaping (A) -> B) -> (Func<R, A>) -> Func<R, B> { return { r2a in return Func { r in f(r2a.apply(r)) } } }

14:49

Can we also define zip ? func zip2<A, B, R>( _ r2a: Func<R, A>, _ r2b: Func<R, B> ) -> Func<R, (A, B)> { return Func<R, (A, B)> { r in (r2a.apply(r), r2b.apply(r)) } } It creates a brand new Func that applies the R input to each of the Func s it zips before returning it in a tuple.

15:59

Now that we have zip2 defined, we can define zip3 the same way we defined our other zip3 s. func zip3<A, B, C, R>( _ r2a: Func<R, A>, _ r2b: Func<R, B>, _ r2c: Func<R, C> ) -> Func<R, (A, B, C)> { return zip2(r2a, zip2(r2b, r2c)) |> map { ($0, $1.0, $1.1) } }

16:18

And we can likewise define zip2(with:) and zip3(with:) . func zip2<A, B, C, R>( with f: @escaping (A, B) -> C ) -> (Func<R, A,>, Func<R, B>) -> Func<R, C> { return { zip2($0, $1) |> map(f) } } func zip3<A, B, C, D, R>( with f: @escaping (A, B, C) -> D ) -> (Func<R, A>, Func<R, B>, Func<R, C>) -> Func<R, D> { return { zip3($0, $1, $2) |> map(f) } }

16:45

Let’s see how we can use this. We can start with something super basic and just wrap up some constants in a closure and pass em to zip2(with:) : zip2(with: +)(Func { 2 }, Func { 3 }) // Func<(), Int>

17:05

Note that the type of each of these Func values is being inferred as Func<Void, Int> , which is really just a wrapper around functions of the form () -> Int . These functions are kinda like lazy values. The closure holds a value, but you never actually execute it or do any work until you hit it with .apply() . zip2(with: +)(Func { 2 }, Func { 3 }) .apply(()) // 5

17:30

Let’s up the ante a bit. Let’s define lazy values that actually make networks requests to fetch data, starting with one that fetches a random number from a web page: let randomNumber = Func<Void, Int> { ( try? String( contentsOf: URL( string: "https://www.random.org/integers/?num=1&min=1&max=30&col=1&base=10&format=plain&rnd=new" )! ) ) .map { $0.trimmingCharacters(in: .newlines) } .flatMap(Int.init) ?? 0 }

18:15

This is completely lazy. It doesn’t execute a network request unless we call apply . randomNumber.apply(()) // 4

18:33

Let’s define one more lazy Func . This one hits the Point-Free homepage and plucks out a seemingly random word. let aWordFromPointFree = Func<Void, String> { ( try? String( contentsOf: URL(string: "https://www.pointfree.co")! ) ) .map { $0.split(separator: " ")[1566] } .map(String.init) ?? "PointFree" }

19:04

Another lazy value that doesn’t run unless we call apply . aWordFromPointFree.apply(()) // "“functional”"

19:23

If we then zip these values up with the Array.init(repeating:count:) function we will get an array that repeats the word fetched from the point-Free website repeated the number of times as the number we fetched from the random number API: zip2( with: [String].init(repeating:count:) )(aWordFromPointFree, randomNumber) // Func<(), Array<String>> This value popped up pretty quickly because none of the lazy values have been evaluated.

20:24

When we call apply , we get a random-length array of our favorite word back. zip2( with: Array.init(repeating:count:) )(aWordFromPointFree, randomNumber) .apply(()) // ["“functional”", "“functional”", "“functional”"]

20:32

This is pretty cool! We were able to take an existing function from the Swift standard library and lift it into the world of lazy values. Zip on other types: F3

20:52

There was another weird type we considered in our episode on map , we called it F3 and we’re still not going to give it a proper name. We’re going to save that for a future episode.

21:31

Its definition is the following: struct F3<A> { let run: (@escaping (A) -> Void) -> Void }

21:43

We observed in that episode that this signature pops up surprisingly often, in particular whenever callbacks are involved, like completion blocks for UIView animation API and URLSession .

22:02

We also showed it had a pretty straightforward map operation: func map<A, B>(_ f: @escaping (A) -> B) -> (F3<A>) -> F3<B> { return { f3 in return F3 { callback in f3.run { callback(f($0)) } } } }

22:48

Does this type also support a zip operation? Well, let’s start with the signature: func zip2<A, B>(_ a: F3<A>, _ b: F3<B>) -> F3<(A, B)> { } It may be hard to see how we can implement this function, so let’s take it one step at a time.

23:16

At the very least we know we need to return an F3 value whose initializer takes a closure that takes a callback as an argument: func zip2<A, B>(_ fa: F3<A>, _ fb: F3<B>) -> F3<(A, B)> { return F3 { callback in } }

24:21

Inside this block we have access to fa and fb , each of which are wrappers around callback functions of the form ((A) -> Void) -> Void . One thing we can do with such functions is run them so that we can eventually get the value that they hold: func zip2<A, B>(_ fa: F3<A>, _ fb: F3<B>) -> F3<(A, B)> { return F3 { callback in fa.run { a in } fb.run { b in } } }

24:45

Well, each of these new blocks has access to an honest A and B , which is precisely what we want to feed into our callback function. But unfortunately we don’t have access to those values at the same time. They are done completely independently.

25:02

If we nest them we get access to both values: func zip2<A, B>(_ fa: F3<A>, _ fb: F3<B>) -> F3<(A, B)> { return F3 { callback in fa.run { a in fb.run { b in callback((a, b)) } } } }

25:18

And we’ve implemented zip on F3 !

25:22

However, there’s something strange here. We haven’t yet given a proper name for F3 , but we’ve alluded to the fact that its related to callbacks, like what we’ve encountered with animation blocks and URLSession . Callbacks have the nice property that they kinda decouple execution flow. You’re given a callback function and you get to call that anytime you want, so whoever is listening to that callback gets the data whenever you decide.

25:51

So, in that sense callbacks are very free in the order in which they execute, yet here we are doing something very specific. We are run ing these values in a sequential order. We first run fa , then once we have a value in a , we run fb , and then we invoke the callback. Perhaps we can loosen this a bit.

26:17

What if we tried to run fa and fb at the same time, independently, like how we started out, and only the one to finish last will invoke the callback. What would that look like? Let’s go back to our earlier example. func zip2<A, B>(_ fa: F3<A>, _ fb: F3<B>) -> F3<(A, B)> { return F3 { callback in fa.run { a in } fb.run { b in } } }

26:45

So what if we kept around some mutable variables outside the run blocks and then stored the results of the run blocks? func zip2<A, B>(_ fa: F3<A>, _ fb: F3<B>) -> F3<(A, B)> { return F3 { callback in var a: A? var b: B? fa.run { a = $0 } fb.run { b = $0 } } }

27:06

So now when each fa and fb run completes we will store the resulting a and b values. Now we detect which one finished last, because that will be the one that is capable of invoking callback since it will have honest A and B values. To do that we only have to try to unwrap the optional values: func zip2<A, B>(_ fa: F3<A>, _ fb: F3<B>) -> F3<(A, B)> { return F3 { callback in var a: A? var b: B? fa.run { a = $0 if let b = b { callback(($0, b)) } } fb.run { b = $0 if let a = a { callback((a, $0)) } } } }

27:56

And we’ve now found another implementation of zip on the F3 type, and it is quite different from the other zip . Moreover, this latter zip somehow seems more “correct” as it takes advantage of some key characteristics of the F3 type, in particular it decouples the execution flow.

28:22

This is similar to what we encountered with the Validated type. There was a zip operation on it that didn’t seem quite right because it unnecessarily threw away information, but luckily there was another zip that seemed to better take advantage of what Validated had to offer.

28:38

There is a very deep explanation of why some of these types seem to have multiple zip operations, even though they only have one map function, but we can’t get to that just yet.

29:05

However, we can go ahead and define zip2(with:) as well as zip3 on this type: func zip2<A, B, C>( with f: @escaping (A, B) -> C ) -> (F3<A>, F3<B>) -> F3<C> { return { zip2($0, $1) |> map(f) } } func zip3<A, B, C>(_ fa: F3<A>, _ fb: F3<B>, _ fc: F3<C>) -> F3<(A, B, C)> { return zip2(fa, _zip2(fb, fc)) |> map { ($0, $1.0, $1.1) } } func zip3<A, B, C, D>( with f: @escaping (A, B, C) -> D ) -> (F3<A>, F3<B>, F3<C>) -> F3<D> { return { zip3($0, $1, $2) |> map(f) } }

29:27

And we can use them pretty easily. Let’s package up some values inside the F3 type: let anInt = F3<Int> { callback in callback(42) } let aMessage = F3<String> { callback in callback("Hello!") }

29:56

And then we can zip them together: zip2(aMessage, anInt) // F3<(String, Int)> This is an all new F3 value that holds the pair of the message and the int. We don’t even know what that means, we’re just working with this abstractly.

30:09

We could change this to be a zip(with:) but then we need to supply a transformation of the string an int: zip2(with: Array.init(repeating:count:))( aMessage, anInt ) // F3<Array<String>>

30:31

What does this new F3 value really represent? Well, the only thing defined on it is this run method: zip2(with: Array.init(repeating:count:))( aMessage, anInt ).run // (([String]) -> ()) -> () It takes a closure as an argument and returns nothing, but the closure it takes also takes an argument, which is an array of strings. Let’s print that array: zip2(with: Array.init(repeating:count:))( aMessage, anInt ) .run { value in print(value) } // ["Hello!", "Hello!", "Hello!", …]

30:50

And lo and behold, we get an array of 42 "Hello" s.

31:08

But here’s the real magical part. As we have noted, callbacks are really useful for decoupling execution order. So we could do something weird like add delays to our anInt and aMessage values, and then would all continue to work. Here’s a helper function that delays an execution block by a duration and prints some logging along the way. func delay( by duration: TimeInterval, line: UInt = #line, execute: @escaping () -> Void ) { print("delaying line \(line) by \(duration)") DispatchQueue.main.asyncAfter(deadline: .now() + duration) { execute() print("executed line \(line)") } }

31:49

Now we can delay all our F3 callbacks. let anInt = F3<Int> { callback in delay(by: 0.5) { callback(42) } } let aMessage = F3<String> { callback in delay(by: 1) { callback("Hello!") } } zip2(with: Array.init(repeating:count:))( aMessage, anInt ) .run { value in print(value) } // delaying line 287 by 1.0 // delaying line 282 by 0.5 // executed line 282 // ["Hello!", "Hello!", "Hello!", …] // executed line 287 And we can see by the order of the logs that the F3 defined with a shorter duration finished first.

32:52

It’s worth mentioning that this implementation of zip2 on F3 is not production-ready because there is a race condition. We don’t know what threads these run closures are being run on, and so it’s possible that two different threads could enter the run s at the same time, causing both if statements to be true or false at the same time, which means either the callback would be called twice or not called at all. We need to fix that, but we are going to save that for another time because it’s not the real point of this exercise.

33:43

What’s really cool is that we’ve now found out that 5 very different types all share a zip operation, and the signatures of these functions all look very similar: ((A, B) -> C) -> ([A], [B]) -> [C] ((A, B) -> C) -> ( A?, B?) -> C? ((A, B) -> C) -> (Validated<E, A>, Validated<E, B>) -> Validated<E, C> ((A, B) -> C) -> (Func<R, A>, Func<R, B>) -> Func<R, C> ((A, B) -> C) -> (F3<A>, F3<B>) -> F3<C> We have five zip(with:) function signatures here, and they all look almost exactly the same: they lift functions (A, B) -> C into functions with the container type wrapping each value. What’s the point?

34:44

So, I think this is right when we should ask “what’s the point?”. The zip on array is clearly useful, the zip on optionals is questionable since Swift did at least catch up to functional programming and give us their own version of that. But the Validated type isn’t super popular in Swift so can it be useful? And zip ing functions seems really weird, and zipping the F3 values even weirder, so it’s hard to see how that would be useful. However, we’ve covered so much in this episode that we want to take a moment to breath and let things marinate. We will be back next week to devote and entire episode to “what’s the point?” References Validated Brandon Williams & Stephen Celis • Aug 17, 2018 Validated is one of our open source projects that provides a Result -like type, which supports a zip operation. This means you can combine multiple validated values into a single one and accumulate all of their errors. https://github.com/pointfreeco/swift-validated Downloads Sample code 0024-zip-pt2 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 .