Video #23: The Many Faces of Zip: Part 1
Episode: Video #23 Date: Jul 23, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep23-the-many-faces-of-zip-part-1

Description
The zip function comes with the Swift standard library, but its utility goes far beyond what we can see there. Turns out, zip generalizes a function that we are all familiar with, and it can unify many seemingly disparate concepts. Today we begin a multipart journey into exploring the power behind zip.
Video
Cloudflare Stream video ID: 04f208fae208242584a6c1d80e203b80 Local file: video_23_the-many-faces-of-zip-part-1.mp4 *(download with --video 23)*
Transcript
— 0:05
On this series we’ve done a number of “deep dives” to really analyze a topic. Things like exploring the map function to see that it’s a very universal idea and we should be comfortable defining it on our own types. And things like “ contravariance ”, which is kind of unintuitive at first but is really quite handy.
— 0:27
This often helps us see something we think we’re familiar with in a new light, or helps us see something that is very unintuitive but become comfortable with it because we develop ways to concisely manage it and transform it.
— 0:43
We’re doing another one of those today, and this time it’s the zip function. You’ve probably used zip a few times in your every day coding, and you probably thought it was pretty handy. But what we want to show people is that it’s a generalization of something you are already familiar with, but you may have never thought of it like this before. And once you see this generalization it really helps unify a bunch of disparate ideas.
— 1:12
Let’s begin by exploring the zip that comes with Swift’s standard library. The standard library zip
— 1:23
The standard library’s zip is a free function with the following signature: public func zip<Sequence1, Sequence2>( _ sequence1: Sequence1, _ sequence2: Sequence2 ) -> Zip2Sequence<Sequence1, Sequence2> where Sequence1 : Sequence, Sequence2 : Sequence { }
— 1:37
We’ve said it a few times on this series, but although at first glance it may seem that Swift shies away from free functions, it actually has quite a few of them, zip being one of them.
— 1:48
This function signature is quite general in that it operates on two Sequence s, which is the most general notion Swift has for having a bunch of values. It returns this Zip2Sequence , which is a which is just a fancy wrapper around a sequence of pairs (A, B) where the first component is a value from the first sequence and the second component is a value from the second sequence: public func zip<Sequence1: Sequence, Sequence2: Sequence>( _ sequence1: Sequence1, // A _ sequence2: Sequence2 // B ) -> Zip2Sequence<Sequence1, Sequence2> // (A, B)
— 2:20
The idea of this function is to take two sequences, walk forward through their values, and then pair them off one-by-one to get a whole new sequence.
— 2:30
The reason this is called zip is to draw an analogy with an actual physical zipper you would find on clothing, in which the teeth on each side make up a pairwise correspondence.
— 2:43
For example: zip([1, 2, 3], ["one", "two", "three"]) // Zip2Sequence<Array<Int>, Array<String>>
— 2:52
It’s hard to see what values this thing really holds because it’s wrapped in this weird type, so let’s convert it to a regular array: Array(zip([1, 2, 3], ["one", "two", "three"])) // [(.0 1, .1 "one"), (.0 2, .1 "two"), (.0 3, .1 "three")]
— 3:05
Cool, so we’re definitely getting the pairings of the sequences.
— 3:16
This can be a pretty handy function. It helps you concisely express the coordination between two sequences. For example, if you have an sequence and you want to iterate over it, but also with each element’s index you can simply do: let xs = [2, 3, 5, 7, 11] Array(zip(xs.indices, xs)) // [(0, 2), (1, 3), (2, 5), (3, 7), (4, 11)]
— 3:57
Note that this is the most general, correct way to accomplish this. There is also an enumerated method, which seems to do something similar: Array(zip(xs.indices, xs)) // [(0, 2), (1, 3), (2, 5), (3, 7), (4, 11)] Array(xs.enumerated()) // [(0, 2), (1, 3), (2, 5), (3, 7), (4, 11)]
— 4:12
And a lot of folks reach for this method when they want to work with a collection’s indices, but this is not what enumerated does. It only gives you counts from 0 and up, which isn’t always correct: let ys = xs.suffix(2) // [7, 11] Array(zip(ys.indices, ys)) // [(3, 7), (4, 11)] Array(ys.enumerated()) // [(0, 7), (1, 11)]
— 4:42
Here we can very clearly see that zipping a sequence with its indices is not the same as calling enumerated on the sequence, and could lead to a crash. We don’t want to go too deeply into why this is the case, but let it be know that zip is very handy for this.
— 5:22
Here’s another fun zip trick. If you zip a sequence with itself but with the first value dropped, you are led to a sequence that holds each value and its predecessor: Array(zip(xs.dropFirst(1), xs)) // [(3, 2), (5, 3), (7, 5), (11, 7)]
— 5:58
A cool thing is that zip automatically figures out which sequence is shorter and iterates only up to the minimum size of both sequences. It bakes that safety right into its definition.
— 6:12
You may have noticed that this array of integers is the first 5 prime numbers. There’s a particular type of prime number known as a “twin” prime, in which the difference between it and the previous prime is only 2. This zip trick here can be a concise way to determine which of these primes is a twin prime: zip(xs.dropFirst(1), xs).forEach { p, q in p - q == 2 ? print("\(p) & \(q) are twin primes!") : print("\(p) & \(q) are NOT twin primes :(") } // 3 and 2 are NOT twin primes :( // 5 and 3 are twin primes! // 7 and 5 are twin primes! // 11 and 7 are NOT twin primes :(
— 7:18
It’s worth noting that the alternative way to do this might look something like: for idx in xs.indices { let p = xs[idx] let q = xs[xs.index(before: idx)] p - q == 2 ? print("\(p) & \(q) are twin primes!") : print("\(p) & \(q) are not twin primes :(") } Execution was interrupted
— 7:56
Uh oh, we got a crash! We start with the first index but try to subscript using an index before it, which is invalid and crashes. What we really want to do is start from the second index, which we can do using dropFirst . for idx in xs.indices.dropFirst() { let p = xs[idx] let q = xs[xs.index(before: idx)] p - q == 2 ? print("\(p) & \(q) are twin primes!") : print("\(p) & \(q) are not twin primes :(") }
— 8:12
There’s a lot of noise there and we are doing some scary stuff with juggling indices that is very easy to mess up. All that index stuff is inherently unsafe since its on us to make sure we are figuring out the index path correctly.
— 8:53
The safety we get with zip can also lead to more expressive code.
— 9:06
For example, say we had an array of episode titles: let titles = [ "Functions", "Side Effects", "Styling with Functions", "Algebraic Data Types" ]
— 9:10
And we wanted to make an array of nicely formatted, enumerated strings starting at 1: // 1.) Functions // 2.) Side Effects // 3.) Styling with Functions // 4.) Algebraic Data Types
— 9:19
We might be tempted to reach for enumerated : titles .enumerated() .map { n, title in "\(n).) \(title)" } // [ // "0.) Functions", // "1.) Side Effects", // "2.) Styling with Functions", // "3.) Algebraic Data Types" // ]
— 9:33
Well that isn’t right because enumerated starts at 0 , so we need to add 1 : titles .enumerated() .map { n, title in "\(n + 1).) \(title)" } // [ // "1.) Functions", // "2.) Side Effects", // "3.) Styling with Functions", // "4.) Algebraic Data Types" // ]
— 9:43
But we’ve now moved that logic into the string interpolation where it’s easy to miss. Instead, we can use zip ’s safety feature to express what we want at a very high level: zip(1..., titles) .map { n, title in "\(n).) \(title)" } // [ // "1.) Functions", // "2.) Side Effects", // "3.) Styling with Functions", // "4.) Algebraic Data Types" // ] This is now equivalent to the enumerated version, but is very clear that we want counts that start at 1 . Defining our own zip
— 10:20
So, that’s the standard library zip . It’s very useful to be familiar with it because it can great simplify some of your everyday code and make it safer and more expressive.
— 10:31
Now you might have noticed that a few times we wrapped the results of zip with Array , and that’s so that its elements are printed out more nicely. This is because zip returns this strange Zip2Sequence wrapper type. It also works with two different generic sequence types. Altogether, this structure kinda hides what is really going on.
— 10:48
In order to really understand what zip means and what properties it has we need to make it a lil less “general” by defining it only for arrays instead of the full Sequence protocol.
— 11:00
To do this we will give it a new name and define it from scratch so that there is no mystery about how easy it is to define this: 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 } zip2([1, 2, 3], ["one", "two", "three"]) // [(1, "one"), (2, "two"), (3, "three")]
— 12:22
We can also easily define zip3 , zip4 , etc., for zipping up even more arrays. Here’s how we might define zip3 by reusing zip2 : 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) } } We merely apply it twice and use map to flatten the nested tuple.
— 13:28
And using it is quite straightforward. zip3([1, 2, 3], ["one", "two", "three"], [true, false, true]) // [(1, "one", true), (1, "two", false), (1, "three", true)]
— 13:45
We could continue to define zip4 and zip5 the same way. All we needed was zip2 .
— 14:01
There’s another interesting thing to note when looking at zip2 ’s signature: zip2: ([A], [B]) -> [(A, B)] At a high level zip is really just flipping the order of containers: it transforms a tuple of arrays into an array of tuples .
— 14:31
We’ve said similar things on this series in the past, like in our episode on map : the map of a composition is the composition of the maps . These flipping of relationships is a very deep idea that will be explored in future Point-Free episodes. Zip as a generalization of map
— 15:03
Now here’s something that you may find surprising: we claim that zip is a generalization of the map function you know and love on arrays. You probably never even thought zip and map had anything to do with each other! Now in order for us to get there we need to make an observation.
— 15:39
Almost always when using zip we immediately follow it up with a map on the resulting sequence of tuples. This is because once we have our sequence of tuples we want to transform them into something else. The tuples alone probably are not super useful. Maybe we use the components of the tuple to do some computation like we did for the twin primes, or we use the components to create an instance of some data type we have, or any number of things.
— 16:05
With this in mind, let’s define a zip that has that transformation baked into it from the start: func zip2<A, B, C>( with f: @escaping (A, B) -> C ) -> ([A], [B]) -> [C] { return { zip2($0, $1).map(f) } }
— 17:03
Let’s clear away the syntax a bit and just look at the signature: // zip2(with:): ((A, B) -> C) -> ([A], [B]) -> [C]
— 17:25
It’s kinda like zip2(with:) lifts a function (A, B) -> C up to a function ([A], [B]) -> [C] . This sounds very similar to how we have described map in the past. Let’s put their signatures on top of each other to see what is going on: // map : ((A) -> B) -> ([A]) -> [B] // zip2(with:): ((A, B) -> C) -> ([A], [B]) -> [C]
— 18:03
Now it’s clear that zip2(with:) is nothing more than map that has been generalized to work on functions with two arguments. And in fact, without zip2(with:) such an operation is impossible with map alone. If you only have map at your disposal there is simply no way map over functions that take two arguments.
— 18:25
Meanwhile, using zip2(with:) is straightforward. zip2(with: +)([1, 2, 3], [4, 5, 6]) // [5, 7, 9]
— 18:43
And just like we generalized zip2 to zip3 we can also make a zip3(with:) . And those functions are precisely the generalization of map to work on functions that take 3 arguments. For instance: func zip3<A, B, C, D>( with f: @escaping (A, B, C) -> D ) -> ([A], [B], [C]) -> [D] { return { xs, ys, zs in zip3(xs, ys, zs).map(f) } }
— 19:08
And using this function is straightforward: zip3( with: { $0 + $1 + $2 })([1, 2, 3], [4, 5, 6], [7, 8, 9]) // [12, 15, 18]
— 19:41
Let’s look at zip3(with:) alongside map and zip2(with:) : // map : ((A) -> B) -> ([A]) -> [B] // zip2(with:): ((A, B) -> C) -> ([A], [B]) -> [C] // zip3(with:): ((A, B, C) -> D) -> ([A], [B], [C]) -> [D]
— 19:54
And we see that zip3(with:) is nothing but a function that helps us lift functions of three arguments up into the world of arrays: where we’re given a function that takes three arrays and produces an array. It’s a generalization of map for functions of three arguments!
— 20:17
The fundamental workhorse of all this is zip2 : it’s the unit that we can build zip2(with:) , zip3 , zip3(with:) , etc. Zip on other types
— 20:32
Now that we have cleared the fog surrounding what zip truly represents, and we’ve boiled it down to just this one signature of generalizing map to functions that take multiple arguments, let’s see where else zip might be useful.
— 21:04
Recall that it is not just arrays that have map in the Swift standard library; optionals also have map , and it’s the unique map , the only one that can be defined with its signature. So, considering that zip can be seen as a generalization of map , what would zip look like for optionals?
— 21:15
Let’s start with two optionals by pasting in zip2 on arrays and swapping out each array for an optional: func zip2<A, B>(_ a: A?, _ b: B?) -> (A, B)? { }
— 21:36
Looking at the signature we see that given an A? and a B? , we should be able to produce (A, B)? . To create that pair, we need to first unwrap our optionals. func zip2<A, B>(_ a: A?, _ b: B?) -> (A, B)? { guard let a = a, let b = b else { return nil } return (a, b) }
— 22:05
There’s still a bit of noise in this definition, so let’s take a look at the signature again. // (A?, B?) -> (A, B)? We can again see it’s essentially a way to flip containers: it transforms a tuple of optionals into an optional tuple .
— 22:28
So how do we use this? let a: Int? = 1 let b: Int? = 2 zip2(a, b) // (1, 2)
— 22:46
Given two values in some , we get a new optional tuple. If one of these values is nil , we get nil back: let a: Int? = 1 let b: Int? = nil zip2(a, b) // nil So what we see is that zip2 on optionals is nothing more than a functional version of multiple guard let s.
— 23:06
Now, prior to Swift 2 this would have been handy because Swift did not support multiple if let s in a single line, leading to multiple levels of indentation: if let a = a { if let b = b { } }
— 23:30
This was problematic enough for the community called it the “pyramid of doom” and for Swift 2 to bake a fix into the language. It’s interesting to note, though, that zip was a tool that could have solved this real problem, and it’s based on things we were already familiar with in arrays. And in fact, if you were functionally leaning at the time, you may have even discovered the zip -like function on optionals yourself.
— 24:06
So we have zip2 defined on optionals. Let’s define zip3 . 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) } } We were able to copy and paste zip3 on arrays, change the signature to handle optionals instead of arrays, and everything still worked!
— 24:39
In the Swift 1 days this would introduce an additional level of indentation: let a: Int? = 1 let b: Int? = 2 let c: Int? = 3 if let a = a { if let b = b { if let c = c { } } }
— 24:50
Meanwhile, zip3 continues to solve this problem on a single line, in a single expression: zip3(a, b, c) // (1, 2, 3)
— 24:57
We’ve now seen that zip2 and zip3 are pretty useful on optionals, so how about zip2(with:) ? 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) } }
— 25:22
Again, all we needed to do was change the signature to deal with optionals. The bodies of the functions are identical to those we defined on arrays. Once we defined zip2 , all these other functions are defined the same in terms of it.
— 25:37
We can now use zip2(with:) : zip2(with: +)(a, b) // 3 zip3(with: { $0 + $1 + $2 })(a, b, c) // 6
— 26:08
These zip(with:) functions, like map , allow us to perform work that would otherwise live inside an if let block. if let a = a, let b = b, let c = c { a + b + c } Using if let has another cost. To get to the result of this work, we need to bind it to another variable. let d: Int? if let a = a, let b = b, let c = c { d = a + b + c } d
— 26:43
We’re still not done here. In order to maintain immutability, we need to add an else block in order to set d to nil whenever one of our if let s fail. let d: Int? if let a = a, let b = b, let c = c { d = a + b + c } else { d = nil } d // 6
— 27:02
It compiles but there’s a lot of noise here, even with the improvements Swift 2 brought with multiple if let s on the same line. We have an additional name to bind the result of our work, we have several if let statements to unwrap our values one by one, we have an else statement, and we have a couple different statements for each assignment, all in order to get that new value at the end.
— 27:25
Meanwhile, zip3 did the same work on a single line and in a single expression! No extra variables and no statements. zip3(with: { $0 + $1 + $2 })(a, b, c) What’s the point?
— 27:35
This is pretty amazing! We started by taking a very specific function, the standard library’s zip , and by exploring its structure on arrays, and seeing its connection to map , and because we already know map ’s connection to optional, we ended up with a whole new zip on optionals! And this zip on optionals not only solves a real world problem that was solved with dedicated language sugar, it can even clean up the if let chaining we have today!
— 28:08
This feels like a good point to ask about the point, but map went deep and I think we can go deeper. If zip works for optionals as well as it works for arrays, what could it do with all the other types we defined on map ? After all, this episode is called “The Many Faces of Zip”, but we’ve only covered two faces.
— 28:28
However, we’re going to cut the episode short so that what we have covered so far can marinate a bit. Tune in next time where we define zip on even more types! Downloads Sample code 0023-zip-pt1 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 .