EP 123 · Standalone · Nov 2, 2020 ·Members

Video #123: Fluently Zipping Parsers

smart_display

Loading stream…

Video #123: Fluently Zipping Parsers

Episode: Video #123 Date: Nov 2, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep123-fluently-zipping-parsers

Episode thumbnail

Description

The zip function shows up on many types: from Swift arrays and Combine publishers, to optionals, results, and even parsers! But zip on parsers is a little unlike zip on all of those other types. Let’s explore why and how to fix it.

Video

Cloudflare Stream video ID: a748f24da45ec90bd1f3a36bb920306f Local file: video_123_fluently-zipping-parsers.mp4 *(download with --video 123)*

Transcript

0:05

Last week we concluded our recap of parsers to bring everyone up to speed on what a parser is, how functional programming defines parsers, and how to decompose a large, complex problem into many small pieces. Today we’ll begin talking about some new topics, starting with how to provide a better interface to the zip function on parsers.

0:26

Recall that zip is a parser combinator whose job is to allow you to run many parsers on an input string. Each parser can consume a bit of the input string to produce a value of some first class data type, and once all the zipped parsers have run it delivers a tuple of all those values to you.

0:51

We’ve used the zip function quite a bit already. It’s probably one of the most important tools for building up complex parsers from simpler ones. And interestingly, the idea of “zipping” is not unique to parsing, there are a lot of data structures out there that support zip-like operations. First of all, probably most widely known, the Swift standard library comes with a zip defined on collections, but we’ve seen it also makes sense to define zip on optionals, results, random number generators, asynchronous values, Combine publishers, and more.

1:22

However, there is one thing about zipping parsers that’s a little different from all of those other examples. When parsing we are often consuming a bit from the input string and then completely discarding the result of that parsing. That is, the parser wants to consume some of the input but it doesn’t produce anything of interest to the user of the parser. For example, we’ve many times parsed a literal string off the front of the input, such as consuming a comma or a space, but those parsers only produce a Void value, hence nothing significant.

2:00

This is pretty unique to parsers. We aren’t often dealing with arrays of void values, or random generators of void values, or even asynchronous void values, but for parsers it is legitimately useful to have a void parser, and even quite common. And because of this oddity, our usages of zip when plugging together many parsers looked a little strange.

2:20

So, we are going to take another look at the zip function on parsers and see if we can come up with another formulation of zip that is a little friendlier to parsers. We will discover an API that not only fixes the problem we just described, but is also more fluent in the way it reads, and even requires us to have fewer overloads to handle the kinds of parsers we have been working with so far. The problem with zip

2:44

Let’s quickly take a look at what we think is so problematic about zip in its current form. In this playground we have all of the work we have accomplished with our parsers so far, including the basic parser type, the fundamental operations of map , zip and flatMap , and the basic building blocks of parsers that allowed us to parse some pretty complex string formats.

5:01

But if we look at our invocations of zip we see some strange things. For example: let temperature = zip(.int, "°F") .map { temperature, _ in temperature }

5:21

Here we zip an integer parser with a string literal parser and are forced to further map on the resulting parser just to ignore the Void value that travels along with the literal parser.

5:21

Our next invocation of zip looks similar: let latitude = zip( .double, ", ", northSouth ) .map { latitude, _, latSign in latitude * latSign }

6:15

Once again we immediately follow zip up with a map that has to explicitly ignore a Void value using an underscore _ . We do the same when parsing longitude, and when parsing a coordinate. And when we create a race parser we do it even more: let race = zip( locationName, ", ", money, "\n", coord.zeroOrMore(separatedBy: "\n") ) .map { location, _, entranceFee, _, coordinates in Race( location: String(location), entranceFee: entranceFee, path: coordinates ) }

6:45

This is a bit strange because we needed an overload of zip that takes 5 arguments, but we are discarding two of those arguments. And then when we map on the resulting parser, we use underscores _ to indicate that we don’t want to even consider what values those parsers produced, which is fair because they are just Void values.

7:02

And that’s weirdness with zip : we keep on having to map just to discard some Void values because some parsers produce Void values, which seems somewhat unique to parsers, since it isn’t something we see so often when zipping together arrays, optionals, or Combine publishers.

7:22

This is a very common pattern. We are often going to need to zip together a very large number of parsers, many of which are Void parsers, and that’s really going to balloon the number of overloads we need for zip .

7:59

One workaround for this is to split the large zip into a few smaller zips. So, if you are zipping together 9 parsers you might be able to instead zip 3 parsers, each of which is the zip of 3 parsers. And this will always work because zip is associative, that is you can parenthesize a zip anywhere you like.

8:09

So if you had a big zip like this: // zip(a, b, c, d, e, f, g, h, i)

8:13

You can group subsets of these arguments with zip in any order and nesting you want: // zip(zip(a, b, c), zip(d, e, f), zip(g, h, i))

8:32

All this nested parser is exactly the same because zip is associative. It’s the exact same reason we don’t have to worry about writing parentheses when dealing with adding numbers together, like if you were adding a bunch of variables: // let totalWidth = leadingInset // + leadingPadding // + width // + trailingPadding // + trailingInset

9:13

But, if you did want to add some parentheses, perhaps to make certain groupings more clear, you are free to: // let totalWidth = (leadingInset + leadingPadding) // + width // + (trailingPadding + trailingInset)

9:30

And so this workaround is completely legitimate, and often you may really want to break a huge 10-argument parser down into a few smaller ones. But also, overdoing a parser decomposition can also be problematic in its own ways. It introduces a whole bunch of new parser names, and you want to make sure those names truly communicate their functionality, and it introduces more indirection to figure out exactly what is going on. So we also think there are legitimate use cases for zipping a bunch of parsers together without decomposing. Solving with more overloads

10:45

Now that we see that zip isn’t quite as ergonomic to use as we would hope, let’s fix it. The crux of the problem is that when zipping together parsers we want to sometimes use a parser only to consume a bit of the input string but not extract any value from it.

11:14

So, for example, if we had we have a temperature parser that could parse the double from a string holding a Fahrenheit temperature it would look like this: let temperature = zip(.int, "°F") .map { temperature, _ in temperature }

11:26

What if we had another overload on zip that would allow us to describe when we want to take the result of the parser and when we want to skip: let temperature2 = zip(take: .int, skip: "°F")

11:45

If we had such an overload then this parser would already be a Parser<Double> without needing to map on it and ignore the void with an underscore.

11:53

To define such an overload we can just bake the mapping logic directly into the zip : func zip<A, B>(take a: Parser<A>, skip b: Parser<B>) -> Parser<A> { zip(a, b).map { a, _ in a } }

12:23

This is exactly what we were previously doing at the call site of the temperature parser, we’re just now hiding it deeper.

12:39

But this is going to open up Pandora’s box. If we can take a parser and then skip a parser, surely some day we are going to want to skip a parser and then take a parser. For example, what if we had a parser that parsed out the username of an at-mentioned user? let atMentioned = zip( "@", .prefix(while: { $0.isLetter || $0.isNumber }) ) .map { _, username in username } atMentioned.run("@pointfreeco")

13:03

We would need another overload: func zip<A, B>(skip a: Parser<A>, take b: Parser<B>) -> Parser<B> { zip(a, b).map { _, b in b } }

13:44

So we now have 3 overloads of zip for zipping two parsers together. One for taking the results of both parsers, one for taking the first and skipping the second, and then another for skipping the first and taking the second.

13:56

But what about when we are zipping 3 parsers? We did this for our latitude parser: let latitude = zip(.double, "° ", northSouth) .map { lat, _, latSign in lat * latSign }

14:02

We can always nest zip s, after all they are associative: let latitude = zip( zip(take: .double, skip: "° "), northSouth ) .map { lat, latSign in lat * latSign }

14:24

Heck, now this last map can even be done in the “point-free” style where we don’t even refer to the closures arguments: let latitude = zip( zip(take: .double, skip: "° "), northSouth ) .map(*)

14:37

So that’s pretty cool. This is starting to give us some insight on how properly handling discardable parsers will help us clean up the construction of our parsers. But it’s a bummer to start nesting zip s again. Only way out of this is yet more overloads, in particular we’d like to zip 3 parsers where we take the result of the first, skip the result of the second, and then take the result of the third: func zip<A, B, C>( take a: Parser<A>, skip b: Parser<B>, take c: Parser<C> ) -> Parser<(A, C)> { zip(a, b, c).map { a, _, c in (a, c) } }

15:10

And now we can write our latitude parser as: let latitude = zip( take: .double, skip: "° ", take: northSouth ).map(*)

15:22

Which reads pretty nicely, but if we have a zip overload for take/skip/take, why don’t we have one for all the other combinations? In particular, there are 2^3 such combinations: // zip(take:take:take:) -> Parser<(A, B, C)> // zip(take:take:skip:) -> Parser<(A, B)> // zip(take:skip:take:) -> Parser<(A, C)> // zip(skip:take:take:) -> Parser<(B, C)> // zip(take:skip:skip:) -> Parser<A> // zip(skip:take:skip:) -> Parser<B> // zip(skip:skip:take:) -> Parser<C> // zip(skip:skip:skip:) -> Parser<Void>

15:44

If we keep going down this road we will have 16 overloads for zip on 4 arguments, 32 overloads for zip on 5 arguments, and if we decided to do this for a theoretical 9-argument zip we would need a whopping 512 overloads.

16:06

So, this clearly isn’t scalable. Not only would it be a pain to implement all of these overloads, but it would probably put the Swift type checker through so much pain that we would never be able to compile our code. So it looks like we need to approach this from another angle. Fluent parsers

16:20

Now that we know that skipping void parsers is such a fundamental operation in practice, let’s try to reimagine what zip could look like if we baked in first class support for it.

16:42

What if instead of chaining together multiple parsers by passing them to a massively overloaded zip function like this: let latitude = zip( take: .double, skip: "° ", take: northSouth ) .map(*)

16:51

We could instead us method chaining like this: let latitude = Parser.double .skip("° ") .take(northSouth) .map(*)

17:28

It reads about the same, it’s the same number of lines, and it’s fewer characters. But most importantly, by using methods we only need to consider how to piece together two parsers at a time. We don’t need to worry about the combinatorial explosion of how to handle takes and skips of a bunch of parsers at once. So I think this will greatly reduce our need for overloads.

17:50

To see this let’s try implementing these methods to get this compiling. We can start with the skip method. It operates on a parser of A s, and takes a void parser, and ultimately returns a parser of A s: extension Parser { func skip(_ p: Parser<Void>) -> Self { zip(self, p).map { a, _ in a } } }

18:52

This is just what zip(take:skip:) was doing, but in method form.

19:21

To get the rest of our latitude parser compiling we need to implement the take method. This will do exactly what zip does, just as a method: extension Parser { func take<NewOutput>( _ p: Parser<NewOutput> ) -> Parser<(Output, NewOutput)> { zip(self, p) } }

20:15

This is just zip in method form.

20:33

And now our latitude parser is compiling: let latitude = Parser.double .skip("° ") .take(northSouth) .map(*)

20:03

And with these methods we can begin to clean up our other parsers, as well. For example we can rewrite our temperature parser: let temperature = Parser.int.skip("°F")

21:08

And this is even a little shorter than the style that uses the zip overloads, and much shorter than the style that uses zip and map together. And we think it reads just as nicely. It is saying that we parse a double off the front of the string, and then we parse and skip the "°F" literal.

21:38

We can even make skip a little more general. Right now we are enforcing that we can only skip Void parsers, but maybe sometimes we want to run a parser that produces an actual value but still discard that value. So let’s let skip be generic: extension Parser { func skip<B>(_ p: Parser<B>) -> Parser { zip(self, p).map { a, _ in a } } }

22:02

Let’s convert more parsers to this more fluent style. We can of course update our longitude parser: let longitude = Parser.double .skip("° ") .take(eastWest) .map(*)

22:41

And next we have a coordinate parser right now that parses off each of the latitude and longitude separated by a comma: let coord = zip( latitude, ", ", longitude ) .map { lat, _, long in Coordinate(latitude: lat, longitude: long) }

22:57

This can be converted to the new take and skip type like so: let coord = latitude .skip(", ") .take(longitude) .map { lat, long in Coordinate(latitude: lat, longitude: long) }

23:27

But even better, now that we have removed that _ from the map , we can just pass the initializer of Coordinate directly to map , point-free style: let coord = latitude .skip(", ") .take(longitude) .map(Coordinate.init(latitude:longitude:))

24:00

And if you want to make things even more succinct you can drop the labels from the initializer, especially in cases where it’s quite clear what the arguments are like in the case for coordinates: .map(Coordinate.init)

24:13

So we are seeing we can simplify the syntax of our parsers by getting rid of those void parsers and underscores.

24:41

And just to show off this style a bit more before moving on to more complex parsers, what if in our original parser we had decided to parse the comma a little differently: let coord2 = zip( latitude, ",", " ", longitude ) .map { lat, _, _, long in Coordinate(latitude: lat, longitude: long) }

25:15

Here we have parsed off the comma and the space as separate steps. This may seem silly to do, but it’s actually pretty common because often we don’t want to just parse off a single space, but rather any number of spaces: let coord2 = zip( latitude, ",", Parser.prefix(" ").zeroOrMore(), longitude ) .map { lat, _, _, long in Coordinate(latitude: lat, longitude: long) }

25:57

Then you really would be forced to introduce yet another void parser, and hence yet another underscore.

26:17

However, with the new take and skip style of parsing we can just chain those along no problem: let coord = latitude .skip(",") .skip(Parser.prefix(" ").zeroOrMore()) .take(longitude) .map(Coordinate.init)

26:42

We could maybe even extract the whitespaces parser out to read a bit nicer: let zeroOrMoreSpaces = Parser.prefix(" ").zeroOrMore() let coord = latitude .skip(",") .skip(zeroOrMoreSpaces) .take(longitude) .map(Coordinate.init)

26:54

This compiles and works just like before, and we didn’t even need another overload.

27:15

Let’s convert some more parsers. Next we have the race parser, which is responsible for parsing off the name of the location of the race, the entrance fee, and a list of coordinates for the race’s route: let race = zip( locationName, ", ", money, "\n", coord.zeroOrMore(separatedBy: "\n") ) .map { location, _, entranceFee, _, coordinates in Race( location: String(location), entranceFee: entranceFee, path: coordinates ) }

27:40

Let’s try converting this to the take and skip style: let race = locationName .skip(", ") .take(money) .skip("\n") .take(coord.zeroOrMore(separatedBy: "\n")) .map { location, entranceFee, coordinates in Race( location: String(location), entranceFee: entranceFee, path: coordinates ) }

28:37

Unfortunately this isn’t compiling. To see why let’s back up a bit and remove the map and look at the type of the parse up to that point: let race = locationName .skip(", ") .take(money) .skip("\n") .take(coord.zeroOrMore(separatedBy: "\n")) // Parser<((Substring, Money), [Coordinate])>

29:04

So it appears that we have a nesting problem. We want our parser to produce a tuple of a substring, money and array of coordinates, but really what is has produced is a tuple whose first component is a tuple of substring and money, and whose second component is the array of coordinates.

29:22

This is exactly the problem we encountered with zip long ago, and it’s specifically why we defined the original set of overloads. So, it seems that this new take and skip style doesn’t completely remove the need for overloads, it just diminishes the number of overloads we need. So, let’s see how we can get around this.

29:38

The problem is in the last take : .take(coord.zeroOrMore(separatedBy: "\n"))

29:55

The parser being operated on at this point is of the form Parser<(Substring, Money)> : let race: Parser<(Substring, Money)> = locationName .skip(", ") .take(money) .skip("\n")

30:01

And so whatever value the argument parser produces will just be naively bundled up in a nested tuple of the form ((Substring, Money), [Coordinate]) . It has no ability to flatten that tuple.

30:17

It turns out we can write an overload of take that identifies this exact situation and automatically flattens the tuple under the hood. What we need to do is extend the Parser type when its output value is a tuple: extension Parser where Output == (A, B) { func take }

30:58

This of course doesn’t work because how does Swift even know what A and B are? What we’d like is if we could introduce some generics when opening this extension: extension <A, B> Parser where Output == (A, B) {

31:15

But this isn’t supported in Swift today, although it may someday.

31:23

Instead what we need to do is drop the constraints on the extension, and instead move them to the method we want to define. So the generics become generics on the function and the constraint becomes a constraint on the function’s generics: extension /*<A, B>*/ Parser /*where Output == (A, B)*/ { // 👇 👇 func take <A, B>() -> Parser<???> where Output == (A, B) }

31:49

Now we want this function to take another parser as input, and so we will introduce another generic: extension /*<A, B>*/ Parser /*Output == (A, B)*/ { func take<A, B, C>(_ p: Parser<C>) -> Parser<???> where Output == (A, B) }

32:02

And we want the parser returned to produce tuples of all of A , B and C : extension /*<A, B>*/ Parser /*Output == (A, B)*/ { func take<A, B, C>( _ c: Parser<C> ) -> Parser<(A, B, C)> where Output == (A, B) }

32:18

To implement our new overload we can just call regular zip under the hood and then map to unpack the tuple: extension Parser { func take<A, B, C>( _ c: Parser<C> ) -> Parser<(A, B, C)> where Output == (A, B) { zip(self, c).map { ab, c in (ab.0, ab.1, c) } } }

32:53

And with that overload we will see that our race parser has magically picked up the correct signature, which means we can now map on it to turn the tuple into a Race value: let race = locationName .skip(", ") .take(money) .skip("\n") .take(coord.zeroOrMore(separatedBy: "\n")) .map { location, entranceFee, path in Race( location: String(location), entranceFee: entranceFee, path: path ) }

33:31

The implementation of take may look a little messy, but hopefully generic extensions will be supported some day in Swift, and if you wipe away all the weird syntax you will see it does have the shape we want: // (Parser<(A, B)>) -> (Parser<C>) -> Parser<(A, B, C)>

34:02

Even better, maybe some day Swift will have variadic generics, and we could somehow capture a signature of this form: // (Parser<(A...)>) -> (Parser<Z>) -> Parser<(A..., Z)> This is trying to express the idea that the first parser produces a tuple of any size, and then we ultimately return a new parser with all of the A s and a Z appended at the end.

34:30

And if we wanted to get really fancy we could move the Substring to String conversion up to the location name parser and then go point-free when mapping on the Race initializer: let race = locationName.map(String.init) .skip(", ") .take(money) .skip("\n") .take(coord.zeroOrMore(separatedBy: "\n")) .map(Race.init(location:entranceFee:path:))

35:12

This is looking really nice.

35:21

And we can now very easily enhance this parser with the ability to parse any number of spaces by introducing another skip . let race = locationName.map(String.init) .skip(",") .skip(zeroOrMoreSpaces) .take(money) .skip("\n") .take(coord.zeroOrMore(separatedBy: "\n")) .map(Race.init(location:entranceFee:path:))

35:40

We are now technically zipping together six parsers. We didn’t even have an overload defined for zipping six parsers together. We’re seeing that we just don’t need tons and tons of overloads when working in this style. Zipping from void

36:55

Let’s convert a few more parsers to make sure we’ve thought through all the edge cases of this new style of parsing. We’ve got a bunch of parsers that form our test logs parser. First, one that is reponsible for parsing out to where a test case finishes. We can try to rewrite it in the fluent style: let testCaseFinishedLine = Parser.prefix(through: " (") .take(.double) .skip(" seconds).\n")

38:03

Although this compiles, it’s not the right kind of parser. We want a Parser<Double> but now we have a Parser<(Substring, Double)> , since we picked up that prefix along the way. The result of the first parser is being taken even though we don’t want it to. This is forcing us to explicitly ignore that result in the map : .map { _, seconds in seconds }

38:42

We currently don’t have a way to skip the result from the first parser, only later parsers.

38:52

To handle this situation we will add a top-level, static function to Parser that allows us to declare when we want to skip the first parser: extension Parser { static func skip(_ p: Self) -> Parser<Void> { p.map { _ in () } } } This parser says to run the one passed in, but then ignore whatever value it produced and just return Void .

39:24

And now our parser chain becomes: let testCaseFinishedLine = Parser.skip(.prefix(through: " (")) .take(.double) .skip(" seconds).\n")

39:39

However, now we have a Parser<(Void, Double)> where we want a Parser<Double> . This is a similar error to what we had before, but with an important difference: the first argument is Void . This makes sense because our static skip function couldn’t possibly get rid of the first component of the tuple, after all something has to be returned, but it was able to turn it into a Void . And that gives us just enough information to write an overload of take that operates on Void parsers and completely discards that value: extension Parser where Output == Void { func take<A>(_ p: Parser<A>) -> Parser<A> { zip(self, p).map { _, a in a } } }

40:44

And now this compiles as a Parser<Double> : let testCaseFinishedLine = Parser .skip(.prefix(through: " (")) .take(.double) .skip(" seconds).\n")

41:02

The next parser to clean up figures out where a test case begins, and extracts the test’s cases name: let testCaseStartedLine = zip( .prefix(upTo: "Test Case '-["), .prefix(through: "\n") ).map { _, line in line.split(separator: " ")[3].dropLast(2) }

41:09

We can swap out our zip for some skip s and take s: let testCaseStartedLine = Parser .skip(.prefix(upTo: "Test Case '-[")) .take(.prefix(through: "\n")) .map { line in line.split(separator: " ")[3].dropLast(2) }

41:39

Next we have the fileName parser that extracts the file where the test log came from, and it can be made a bit shorter by using our new operators: let fileName = Parser .skip("/") .take(.prefix(through: ".swift")) .flatMap { filePath in filePath.split(separator: "/").last.map(Parser.always) ?? .never }

42:09

Then we have the body parser, which is responsible for extracting out the file name, test case line number, and failure message. This one can be made quite simpler using the new operators: let testCaseBody = fileName .skip(":") .take(.int) .skip(.prefix(through: "] : ")) .take(.prefix(upTo: "Test Case '-[")) .map { fileName, line, failureMessage in (fileName, line, failureMessage.dropLast()) }

43:07

And now that the map is just doing a little work before re-packaging up a tuple, we can drop the map by moving that work up the chain: let testCaseBody = fileName .skip(":") .take(.int) .skip(.prefix(through: "] : ")) .take(Parser.prefix(upTo: "Test Case '-[").map { $0.dropLast() })

43:26

And finally we have the parsers for test failures and passes. Both work perfectly fine with zip since we are taking the results of all the parsers being zipped, but if we wanted to convert it would look something like this: let testFailed = testCaseStartedLine .take(testCaseBody) .take(testCaseFinishedLine) .map { testName, bodyData, time in TestResult.failed( failureMessage: bodyData.2, file: bodyData.0, line: bodyData.1, testName: testName, time: time ) } let testPassed = testCaseStartedLine, .take(testCaseFinishedLine) .map(TestResult.passed(testName:time:))

44:32

So even where zip was working just fine, the fluent style can read nicely in a row since you know exactly where we are taking new data into the parser. What’s the point?

44:45

It’s been really nice to see how are parsers have been upgraded from using zip and having to explicitly ignore void values in map into a fluent series of steps that can be read where we take some values we parse out and skip over others. We did need a few up-front overloads to handle various cases, but overall we need fewer and fewer overloads than we would have needed had we stuck with zip .

45:16

Let’s take a moment to reflect on what we have accomplished here. We’ve replaced a bunch overloads of the zip function with a couple of new methods that are fine tuned for running a parser and then either keeping or skipping the result of that parser.

45:39

Now technically we haven’t actually reduced the number of functions we need to write the parsers in this playground right now. Previously we had 5 zip overloads for handling up to 5 parsers at time, and now we have 5 skip and take methods: extension Parser { func skip<B>(_ p: Parser<B>) -> Parser { … } func take<B>(_ p: Parser<B>) -> Parser<(A, B)> { … } func take<A, B, C>(_ c: Parser<C>) -> Parser<(A, B, C)> where A == (A, B) { … } static func skip(_ a: Self) -> Parser<Void> { … } func take<B>(_ p: Parser<B>) -> Parser<B> { … } }

47:00

However, these 5 methods are capable of composing many more parsers without the need for even more overloads. The skip - take style is only bounded by the number of concrete, non-void values you want to zip together.

47:38

In contrast, zip needed overloads for however many parsers we operate on, regardless of if they have void data or not.

48:08

For example, if we wanted to parser 3 comma separated numbers with any number of spaces between then we would ned the following zip : // ( 1 , 2 , 3 ) zip( "(", zeroOrMoreSpaces, .double, zeroOrMoreSpaces, ",", zeroOrMoreSpaces, .double, zeroOrMoreSpaces, ",", zeroOrMoreSpaces, .double, zeroOrMoreSpaces, ")" )

48:50

That is piecing together 13 parsers, which would require a zip overload that takes 13 parsers even though we only care about the value produced from 3 of them. And then we’d have to map on it to ignore all the void values we don’t care about.

49:10

On the other hand, doing this in the take - skip style of parsing already works with the few overloads we’ve already defined and there’s no need to further map onto it to get things into the shape we want: // ( 1 , 2 , 3 ) Parser.skip("(") .skip(zeroOrMoreSpaces) .take(Parser.double) .skip(zeroOrMoreSpaces) .skip(",") .skip(zeroOrMoreSpaces) .take(.double) .skip(zeroOrMoreSpaces) .skip(",") .skip(zeroOrMoreSpaces) .take(.double) .skip(zeroOrMoreSpaces) .skip(")") // Parser<(Int, Int, Int)>

50:03

So this is showing that the new skip and take style of parsing is not only more fluent, but will drastically reduce the number of overloads we need to define in the future. Essentially the number of overloads that need to be defined is bounded by the number of values we want to extract from the input. So if you need to parse a string to get 3 values out of it, we only need 3 overloads even though you may be skipping over 10 other parsers. Whereas with zip you need an overload for every number of parsers you consider. So even if you are skipping 10 parsers and taking 1, you will need an overload for 11 parsers.

50:48

So that’s the basics of how to fluently zip with parsers. It’s right around this time that we would ask “what’s the point?” so that we can bring the things we discuss back down to earth and show some real world applications, but that isn’t really necessary this time. We addressed a pretty obvious ergonomic pitfall of the zip function as it pertains to parsers, and we fixed it in a pretty nice way. Downloads Sample code 0123-fluently-zipping-parsers 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 .