Video #175: Parser Builders: The Point
Episode: Video #175 Date: Jan 24, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep175-parser-builders-the-point

Description
So what is the point of parser builders anyway? We will leverage our new builder syntax by rewriting a couple more complex parsers: a marathon parser and a URL router. This will lead us to not only clean up noise and tell a more concise parsing story, but give us a chance to create brand new parsing tools.
Video
Cloudflare Stream video ID: f29448d65923b0592947d519155623ce Local file: video_175_parser-builders-the-point.mp4 *(download with --video 175)*
References
- Discussions
- we’ve reported it
- a new version
- Swift Parsing
- Declarative String Processing
- 0175-parser-builders-pt3
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
So we now have the basic infrastructure in place to start seeing what parser builders give us over the take/skip fluent style of parsing that we developed previously, and so far it’s pretty promising.
— 0:15
However, on Point-Free we like to frequently ask the question “What’s the point?” so that we can bring things back down to earth and make sure that what we are doing is really worth it. In the case of result builders there is a strong inclination to use this new Swift feature to tackle problems that don’t really need a comprehensive DSL or builder syntax. Do parsers really meet the requirements necessary to justify using result builders?
— 0:40
And we think definitely, yes. So far we have only parsed a simple textual format, but the dividends really start to pay when we tackle more complex problems. This gives us more opportunities to clean up noise and have the parsers tell a more concise story, and gives us a chance to create new parsing tools that leverage builder syntax.
— 1:02
So, Let’s start flexing those muscles by taking a look at some of the parser demos that come with the library to see what other cool things there are to discover.
— 1:13
If you didn’t know this already, the library comes with a large collection of parsers and benchmarks. If we expand the swift-parsing-benchmark directory we will find 16 different benchmarks that test a variety of things. There are a lot of fun things in these benchmarks, including and arithmetic expression parser, a binary data parser, an HTTP parser, a router, and a lot more. Marathon parser builder
— 1:41
Let’s start with a parser that should be near and dear to us, which is the marathon race parser that we used in all of our previous episodes on parsing in order to motivate the design of the library.
— 1:56
It aims to parse a custom text format that describes a collection of marathon races, where each race is represented by a location, entrance feed and collection of coordinates for the path of the race: let input = """ New York City, $300 40.60248° N, 74.06433° W 40.61807° N, 74.02966° W … --- Berlin, €100 13.36015° N, 52.51516° E 13.33999° N, 52.51381° E … --- London, £500 51.48205° N, 0.04283° E 51.47439° N, 0.02170° E 51.47618° N, 0.02199° E … """
— 2:16
We built a parser that is capable of processing this string by defining a bunch of tiny parsers and then piecing them together to form one big parser.
— 2:23
To begin, we define some tiny parsers that are concerned only with parsing the “N”, “S”, “E” or “W” from a coordinate. This is because geographic coordinates are specified by a non-negative double and a cardinal direction, and the direction determines whether the coordinate is positive or negative. We accomplish this using the .orElse operator: private let northSouth = "N".utf8.map { 1.0 } .orElse("S".utf8.map { -1 }) private let eastWest = "E".utf8.map { 1.0 } .orElse("W".utf8.map { -1 })
— 2:52
One thing to note about these parsers is that we are parsing on the level of UTF8View s rather than Substring s. So, for example, the "S".utf8 parser is one that parsers the beginning of a UTF8View , which is a collection of code units, to match the code units for "S" .
— 3:11
This isn’t something we have seen yet on this series of episodes, but it is something we discussed at great length in past episodes when exploring performance . We saw that we could squeeze out substantial performance gains by dealing with lower level string representations, such as UTF8 code units rather than extended grapheme clusters. Working on the lower level string representations comes with some complexities and subtleties that you have to keep in mind. If you are willing to do that you will get some performance boosts out of your parsers, and otherwise you can always just parse simple substrings.
— 3:44
We should now be able to rewrite these parsers using the OneOf builder: private let northSouth = OneOf { "N".utf8.map { 1.0 } "S".utf8.map { -1 } } private let eastWest = OneOf { "E".utf8.map { 1.0 } "W".utf8.map { -1 } } Static method ‘buildBlock’ requires the types ‘Double’ and ‘Int’ be equivalent
— 4:55
The first error appears to be that migrating to builder syntax has broken some type inference. As far as we can tell this is a bug in result builders, and we’ve reported it and hope it gets fixed soon, but in the meantime we can get around it by making the types a bit more explicit to the compiler by using double literals everywhere. private let northSouth = OneOf { "N".utf8.map { 1.0 } "S".utf8.map { -1.0 } } private let eastWest = OneOf { "E".utf8.map { 1.0 } "W".utf8.map { -1.0 } } Missing argument for parameter #3 in call
— 5:22
And now this doesn’t yet compile because we only defined the buildBlock for 3 parsers, and here we are dealing with only two. To support this situation we can implement another buildBlock overload: extension OneOfBuilder { static func buildBlock<P0, P1>( _ p0: P0, _ p1: P1 ) -> Parsers.OneOf<P0, P1> { p0.orElse(p1) } }
— 5:54
And now the northSouth and eastWest parsers are compiling.
— 6:00
Next we’ve got the latitude and longitude parsers, which are responsible for parsing off a double, then skipping the degree symbol, and then finally parsing the “N”, “S”, “E” or “W” to turn it into a plus or minus sign: private let latitude = Double.parser() .skip("° ".utf8) .take(northSouth) .map(*) private let longitude = Double.parser() .skip("° ".utf8) .take(eastWest) .map(*)
— 6:16
We hope that we could convert this into builder syntax like so: private let latitude = Parse(*) { Double.parser() "° ".utf8 northSouth } private let longitude = Parse(*) { Double.parser() "° ".utf8 eastWest }
— 6:56
However, this doesn’t work. We do have an overload of parser builders that takes 3 parsers, but that overload currently takes the output of each of the parsers. Here we have a situation where we need to run 3 parsers, but we want to skip the output of the middle one.
— 7:10
So, we need yet another overload of buildBlock to do this: extension ParserBuilder { static func buildBlock<P0, P1, P2>( _ p0: P0, _ p1: P1, _ p2: P2 ) -> Parsers.Take2<Parsers.SkipSecond<P0, P1>, P2> where P0: Parser, P1: Parser, P2: Parser, P1.Output == Void { p0.skip(p1).take(p2) } }
— 7:28
And now our latitude and longitude parsers are building.
— 7:33
Next we have a parser for extracting out a full geographic coordinate, which consists of parsing off a latitude, then a comma and any number of spaces, then the longitude, and then finally bundling up those doubles into a Coordinate value: private let coord = latitude .skip(",".utf8) .skip(zeroOrMoreSpaces) .take(longitude) .map(Coordinate.init)
— 7:50
We hope we can write this simply in the parser builder style: private let coord = Parse(Coordinate.init) { latitude ",".utf8 zeroOrMoreSpaces longitude }
— 8:10
But again we do not have an overload of buildBlock for this exact situation. We need one that takes 4 parsers, takes the output of the first and last, and skips the output of the middle two. It’s easy enough to define such an overload: extension ParserBuilder { static func buildBlock<P0, P1, P2, P3>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3 ) -> Parsers.Take2< Parsers.SkipSecond<Parsers.SkipSecond<P0, P1>, P2>, P3 > where P0: Parser, P1: Parser, P2: Parser, P3: Parser, P1.Output == Void, P2.Output == Void { p0.skip(p1).skip(p2).take(p3) } }
— 8:30
We must stress that it is not expected that users of the library would have to know about these bizarre overload methods. The library would provide these, and ideally they would be auto-generated, and so users of the library would never need to think about this.
— 8:42
But, with that overload in place, our code still does not compile: Static method ‘buildBlock’ requires the types ‘Substring.UTF8View’ and ‘Void’ be equivalent
— 8:48
This is because the zeroOrMoreSpaces parser is not a Void parser, which is required by the buildBlock overload in order for its output to be discarded. Since it is defined as a Prefix parser, it’s output is actually the amount of the input it was able to consume by the predicate: private let zeroOrMoreSpaces = Prefix { $0 == .init(ascii: " ") }
— 9:05
We need to convert this parser to a Void parser, and luckily it’s quite easy to do by using the Skip parser: private let zeroOrMoreSpaces = Skip( Prefix { $0 == .init(ascii: " ") } )
— 9:14
The Skip parser can turn any parser into one that has Void as an output by simply forgetting about whatever output was produced and just returning Void .
— 9:26
This gets the code compiling, but there is another variation we could consider. Rather than converting zeroOrMoreSpaces to be a Void parser, we could localize the Skip parser to be where we actually need to be void: private let coord = Parse(Coordinate.init) { latitude ",".utf8 Skip(zeroOrMoreSpaces) longitude }
— 9:37
This still compiles.
— 9:39
Further, since we now have parser builder syntax at our disposal, and we like the syntax, perhaps it would be better for the Skip parser to look like this: private let coord = Parse(Coordinate.init) { latitude ",".utf8 Skip { zeroOrMoreSpaces } longitude }
— 9:51
This would make it easier to group multiple parsers together that should be skipped, for example we could skip the comma and spaces in one block like this: private let coord = Parse(Coordinate.init) { latitude Skip { ",".utf8 zeroOrMoreSpaces } longitude }
— 10:04
To get this to work we need to provide an initializer to Skip that is parser builder aware: extension Skip { init(@ParserBuilder upstream: () -> Upstream) { self.init(upstream()) } }
— 10:43
And then to support running two parsers in the Skip closure we need an overload of buildBlock for two parsers: extension ParserBuilder { static func buildBlock<P0, P1>( _ p0: P0, _ p1: P1 ) -> Parsers.Take2<P0, P1> where P0: Parser, P1: Parser { p0.take(p1) } }
— 10:58
And with that defined our parser is already building.
— 11:09
We want to reiterate yet again that we of course do not expect users of the library to define all of these overloads. It’s a huge pain to maintain, and the library should (and will) provide them so that we don’t have to think about it at all while developing new parsers.
— 11:28
Let’s keep converting parsers. Next we have this currency parser, which parses off a few literal strings for currency symbols that we support, and maps them into a first-class Currency enum type: private let currency = "€".utf8.map { Currency.eur } .orElse("£".utf8.map { .gbp }) .orElse("$".utf8.map { .usd })
— 11:38
Luckily for us we already have a OneOf builder overload that takes three parsers, so this converts quite cleanly: private let currency = OneOf { "€".utf8.map { Currency.eur } "£".utf8.map { .gbp } "$".utf8.map { .usd } } Cannot infer contextual base in reference to member ‘gbp’ Cannot infer contextual base in reference to member ‘use’
— 11:56
And due to another result builder type inference bug, possibly the same one we saw before, we need to specify the types a bit more explicitly. private let currency = OneOf { "€".utf8.map { Currency.eur } "£".utf8.map { Currency.gbp } "$".utf8.map { Currency.usd } }
— 12:08
Hopefully this gets fixed soon.
— 12:11
We’re getting very close to converting all parsers to the new builder syntax.
— 12:15
Next we have the money parser, which is responsible for parsing off the currency symbol, then the amount of the entrance fee for the race, and then finally bundles that up into a first-class Money type: private let money = currency.take(Double.parser()) .map(Money.init(currency:value:))
— 12:18
Thanks to an overload of buildBlock we defined a moment ago, this easily translates over to the builder syntax: private let money = Parse(Money.init) { currency Double.parser() }
— 12:39
Next we have the race parse, which does everything necessary to parse a single race from the string. It first gets the location by consuming everything up until the first comma, then it skips the comma and any spaces encountered, then it parses the entrance fee, then skips the new line, and then parses as many coordinates as possible, separated by newlines: private let race = locationName.map { String(Substring($0)) } .skip(",".utf8) .skip(zeroOrMoreSpaces) .take(money) .skip("\n".utf8) .take(Many(coord, separator: "\n".utf8)) .map(Race.init(location:entranceFee:path:))
— 13:09
Naively converting this to the parser builder style gives something like this: private let race = Parse(Race.init) { locationName.map { String(Substring($0)) } ",".utf8 Skip { zeroOrMoreSpaces } money "\n".utf8 Many(coord, separator: "\n".utf8) }
— 13:31
In order for this to work we would need to define yet another buildBlock overload, this time for 6 parsers, which do a take, skip, skip, take, skip, take under the hood.
— 13:42
Coincidentally, if we just group the first two skips into a single Skip parser we will find that we actually already have this overload defined from before: private let race = Parse(Race.init) { locationName.map { String(Substring($0)) } Skip { ",".utf8 zeroOrMoreSpaces } money "\n".utf8 Many(coord, separator: "\n".utf8) }
— 13:56
This is purely a coincidence, and of course we shouldn’t have to think about such things when using the library. The library should, and will, define all the overloads for us so we never have to worry about this.
— 14:05
But, with things compiling we could now also try convert the Many parser to the new builder syntax that we introduced earlier in the episode: private let race = Parse(Race.init) { locationName.map { String(Substring($0)) } Skip { ",".utf8 zeroOrMoreSpaces } money "\n".utf8 Many { coord } separator: { "\n".utf8 } }
— 14:20
And finally we can now convert the last remaining parser in this file to the new builder syntax, which is simply a matter of making the races parser use the builder initializer for the Many parser: private let races = Many { race } separator: { "\n---\n".utf8 }
— 14:38
We have now converted the entire races parser into the new builder syntax, and it should work exactly as it did before. The process was very straightforward, besides the work we needed to do to define buildBlock overloads, which we will not have to do once they are auto-generated for us. URL router builder
— 14:53
So, this is looking really great. It’s quite mechanical to convert old-style parsers to the new builder style, and if we were to write a parser from scratch we would be able to do so in a way that removes a lot of superfluous noise.
— 15:10
Let’s convert one more of the demo parsers from the benchmark to show just how powerful the new builder syntax can be. We have a benchmark showing how to create a basic URL parser using our parser library, which can serve as the foundation of a deep-link router in an iOS application. In fact, in the final episode of our SwiftUI navigation series we used this technique to build a router for powering deep-linking to various screens in a multi-screen application, and we highly recommend watching that episode for a detailed discussion of those concepts.
— 15:51
Let’s take a look at the router we built for the benchmark. It’s goal is to parse an incoming URL request into one of the following routes: enum AppRoute: Equatable { case home case contactUs case episodes case episode(id: Int) case episodeComments(id: Int) }
— 16:03
And it does so by piecing together various URL request parsers, such as the Method parser, which parsers the HTTP method from the request, the Path parser, which parsers a single path component from the URL, and the PathEnd parser, which verifies that there are no more path components to parse: let router = Method("GET") .skip(PathEnd()) .map { AppRoute.home } .orElse( Method("GET") .skip(Path("contact-us".utf8)) .skip(PathEnd()) .map { AppRoute.contactUs } ) .orElse( Method("GET") .skip(Path("episodes".utf8)) .skip(PathEnd()) .map { AppRoute.episodes } ) .orElse( Method("GET") .skip(Path("episodes".utf8)) .take(Path(Int.parser())) .skip(PathEnd()) .map(AppRoute.episode(id:)) ) .orElse( Method("GET") .skip(Path("episodes".utf8)) .take(Path(Int.parser())) .skip(Path("comments".utf8)) .skip(PathEnd()) .map(AppRoute.episodeComments(id:)) )
— 16:25
This is a big parser, and it’s packing a huge punch. It succinctly describes how to parse each route, and does so in a type-safe manner. If we wanted to parse more routes we would just need to add a case to the AppRoute enum, and then add a new .orElse statement to the parser to describe how to parse into that route value.
— 16:51
But, this parser isn’t without its faults. First of all, as we’ve mentioned before, there is a lot of noise in this parser. The repeated .orElse operators, and the explicit .skip s and .take s make it difficult to see exactly what is going on.
— 17:04
But worse, there is some repeated work in every single parser that is very important to perform, and if you forget to you will subtly break the parser. Notice that every single parser used with the .orElse operator has a .skip(PathEnd()) added. This parser is used to prove that we have consumed all of the path components from the URL and that there is nothing left to process.
— 17:29
This is very important to have because if we constructed a parser without it: Method("GET") .skip(Path("episodes".utf8)) // .skip(PathEnd()) .map { AppRoute.episodes }
— 17:44
To parse this URL: /episodes/42
— 17:56
We would get a successful value. That is because we did successfully parse “episodes” from the first path component of the URL, and so we mistakenly think everything must have gone fine and map the result into the AppRoute.episodes route.
— 18:12
However, that is clearly not correct. This parser should fail on this URL because there was another path component remaining that needed to be parsed. This is what the PathEnd parser accomplishes, and this is why we need it at the end once we are done parsing all path components that we care about for a particular route: Method("GET") .skip(Path("episodes".utf8)) .skip(PathEnd()) .map { AppRoute.episodes }
— 18:34
It would be disastrous if we forget the PathEnd parser. By embracing parser builders we can fix all of these problems.
— 18:48
Let’s start with the verbosity problem. To fix that, we just need to convert this over to parser builder syntax. We can begin by converting the chain of .orElse operators to a single top-level OneOf like so: let router = OneOf { Method("GET") .skip(End()) .map { AppRoute.home } Method("GET") .skip(Path("contact-us".utf8)) .skip(PathEnd()) .map { AppRoute.contactUs } Method("GET") .skip(Path("episodes".utf8)) .skip(PathEnd()) .map { AppRoute.episodes } Method("GET") .skip(Path("episodes".utf8)) .take(Path(Int.parser())) .skip(PathEnd()) .map(AppRoute.episode(id:)) Method("GET") .skip(Path("episodes".utf8)) .take(Path(Int.parser())) .skip(Path("comments".utf8)) .skip(PathEnd()) .map(AppRoute.episodeComments(id:)) }
— 19:16
That is already quite nice. Now each route parser lives on the same indentation level so that no single parser is given more prominence than the others.
— 19:27
However, this doesn’t compile right now because we don’t have an overload of buildBlock that takes 5 parsers, so let’s do that real quick: extension OneOfBuilder { static func buildBlock<P0, P1, P2, P3, P4>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4 ) -> Parsers.OneOf< Parsers.OneOf< Parsers.OneOf<Parsers.OneOf<P0, P1>, P2>, P3 >, P4 > { p0.orElse(p1).orElse(p2).orElse(p3).orElse(p4) } }
— 19:55
Now that is compiling, so let’s try converting some of these smaller route parsers to the builder syntax.
— 20:05
The first can maybe be done like so: Parse(AppRoute.home) { Method("GET") PathEnd() }
— 20:20
But this doesn’t compile for a few reasons. First, Parse currently takes a transform function that maps the output to some new output, but AppRoute.home is just a value with no associated data, so we must open up a block instead: Parse({ AppRoute.home }) { Method("GET") PathEnd() }
— 20:49
But this further doesn’t compile because we don’t have an overload of buildBlock that runs two parsers and discards the output of both, so let’s do that: extension ParserBuilder { static func buildBlock<P0, P1>( _ p0: P0, _ p1: P1 ) -> Parsers.SkipSecond<P0, P1> where P0: Parser, P1: Parser, P0.Output == Void, P1.Output == Void { p0.skip(p1) } }
— 21:13
And now this compiles, but the syntax is a little noisy. Right now we are forced to provide a closure to the initializer of Parse that transforms all the outputs of the parsers into some new value. This has worked great in the past, but here we’d like to just supply AppRoute.home directly since that case of the enum doesn’t have any associated values, and hence is not a function: Parse(AppRoute.home) { Method("GET") PathEnd() }
— 21:34
In order to support this we need another initializer on Parse that allows providing a value rather than a function: init( _ newOutput: NewOutput, @ParserBuilder parsers: () -> Parsers ) where Parsers.Output == Void { self.init({ newOutput }, parsers: parsers) }
— 22:28
And now this compiles: Parse(AppRoute.home) { Method("GET") PathEnd() }
— 22:34
We can convert the next similarly: Parse(AppRoute.contactUs) { Method("GET") Path("contact-us".utf8) PathEnd() }
— 22:53
But again, to get this compiling we need to provide another overload of buildBlock : extension ParserBuilder { static func buildBlock<P0, P1, P2>( _ p0: P0, _ p1: P1, _ p2: P2 ) -> Parsers.SkipSecond<Parsers.SkipSecond<P0, P1>, P2> where P0: Parser, P1: Parser, P2: Parser, P0.Output == Void, P1.Output == Void, P2.Output == Void { p0.skip(p1).skip(p2) } }
— 23:13
The next route is very similar: Parse(AppRoute.episodes) { Method("GET") Path("episodes".utf8) PathEnd() }
— 23:30
After that we start to get into some more interesting routes. For example, the .episode route has some associated data, which means we can pass the AppRoute.episode function to the Parse initializer: Parse(AppRoute.episode(id:)) { Method("GET") Path("episodes".utf8) Path(Int.parser()) PathEnd() } And similarly for the .episodeComments route: Parse(Route.episodeComments(id:)) { Method("GET") Path("episodes".utf8) Path(Int.parser()) Path("comments".utf8) PathEnd() }
— 25:04
But to get these compiling we will yet again have to paste in more buildBlock overloads: extension ParserBuilder { static func buildBlock<P0, P1, P2, P3>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3 ) -> Parsers.SkipSecond< Parsers.SkipFirst<Parsers.SkipSecond<P0, P1>, P2>, P3 > where P0: Parser, P1: Parser, P2: Parser, P3: Parser, P0.Output == Void, P1.Output == Void, P3.Output == Void { p0.skip(p1).take(p2).skip(p3) } static func buildBlock<P0, P1, P2, P3, P4>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4 ) -> Parsers.SkipSecond< Parsers.SkipSecond< Parsers.SkipFirst<Parsers.SkipSecond<P0, P1>, P2>, P3 >, P4 > where P0: Parser, P1: Parser, P2: Parser, P3: Parser, P4: Parser, P0.Output == Void, P1.Output == Void, P3.Output == Void, P4.Output == Void { p0.skip(p1).take(p2).skip(p3).skip(p4) } }
— 25:18
Now everything is compiling, and the router is looking much, much nicer. We have cleaned up all of the .orElse , .take and .skip noise, we have moved the route we are parsing into from the bottom to the top, and best of all, we are now poised to introduce some domain-specific parsers to make this code even better.
— 25:43
What if we could cook up another top-level entry point into parsing that was highly tuned for URL request routing? First of all, it could have a better name than Parse . Perhaps Route : Route(AppRoute.home) { Method("GET") PathEnd() }
— 26:00
Just that small change makes helps us understand that we aren’t just doing any kind of parsing, but we are specifically doing URL request parsing.
— 26:08
However, if all this top-level parser offered us was a new name, then it probably wouldn’t be worth our time. By introducing a new parser we can also layer on some additional logic that is only appropriate for the URL parsing domain.
— 26:22
For example, as we mentioned before, it can be quite disastrous for us to forget adding .skip(PathEnd()) to the end of our parsers once we have parsed everything we expect from the path components. Leaving it off can cause us to recognize URLs for routes that are not correct.
— 26:36
What if we could bake in that .skip(PathEnd()) automatically so that whenever you use the Route top-level parser you don’t even have to thinking about skipping the end. That would allow us to write our router in an even simpler and more concise fashion: let router = OneOf { Route(AppRoute.home) { Method("GET") } Route(AppRoute.contactUs) { Method("GET") Path("contact-us".utf8) } Route(AppRoute.episodes) { Method("GET") Path("episodes".utf8) } Route(AppRoute.episode(id:)) { Method("GET") Path("episodes".utf8) Path(Int.parser()) } Route(AppRoute.episodeComments(id:)) { Method("GET") Path("episodes".utf8) Path(Int.parser()) Path("comments".utf8) } }
— 27:00
Now we don’t have to remember to use the PathEnd() parser every time, and the entire router reads much better.
— 27:04
Let’s see what it takes to implement this Route type. It will look quite similar to the Parse type we created before. It’ll be a new struct that is generic over the type of parser that is run on the inside of the builder closure, which needs to operator on RequestData , and the domain-specific output we transform to: private struct Route<Parsers, NewOutput>: Parser where Parsers: Parser, Parsers.Input == RequestData { let transform: (Parsers.Output) -> NewOutput let parsers: Parsers init( _ transform: @escaping (Parsers.Output) -> NewOutput, @ParserBuilder parsers: () -> Parsers ) { self.transform = transform self.parsers = parsers() } init( _ newOutput: NewOutput, @ParserBuilder parsers: () -> Parsers ) where Parsers.NewOutput == Void { self.init({ newOutput }, parsers: parsers) } func parse(_ input: inout Parsers.Input) -> NewOutput? { self.parsers.parse(&input).map(self.transform) } }
— 28:25
While this builds, the routes we removed PathEnd() do not because we need a new buildBlock overload: extension ParserBuilder { static func buildBlock<P0, P1, P2>( _ p0: P0, _ p1: P1, _ p2: P2 ) -> Parsers.SkipFirst<Parsers.SkipSecond<P0, P1>, P2> where P0: Parser, P1: Parser, P2: Parser, P0.Output == Void, P1.Output == Void { p0.skip(p1).take(p2) } }
— 29:11
And now things build, but we need to bake in that .skip(PathEnd()) logic directly into the parse method.
— 29:27
PathEnd simply fails if the input’s path components is empty, so we can perform the same check inside Route ’s parse method: func parse(_ input: inout Parsers.Input) -> NewOutput? { guard let output = self.parsers.parse(&input).map(self.transform), input.pathComponents.isEmpty else { return nil } return output }
— 29:52
But we also need to undo the parsing done by self.parsers if the path components are not empty: func parse(_ input: inout Parsers.Input) -> Output? { let original = input guard let output = self.parsers.parse(&input).map(self.transform), input.pathComponents.isEmpty else { input = original return nil } return output }
— 30:22
Everything compiles, and our router is looking even more amazing. It’s more succinct and readable with less noise, and the whole thing almost fits on the screen all at once.
— 30:33
Now that everything is looking as short as it is, there’s some more repeated noise that sticks out: Method("GET") appears at the beginning of each Route block. The GET method for HTTP requests is generally considered the default. If we don’t specify a method it is assumed to be a GET. Only if we want to POST, PUT, DELETE, or use another HTTP method do we need to specify. Maybe we could further get rid of all the Method("GET") noise and consider it the default: let router = OneOf { Route(AppRoute.home) { } Route(AppRoute.contactUs) { Path("contact-us".utf8) } Route(AppRoute.episodes) { Path("episodes".utf8) } Route(AppRoute.episode(id:)) { Path("episodes".utf8) Path(Int.parser()) } Route(AppRoute.episodeComments(id:)) { Path("episodes".utf8) Path(Int.parser()) Path("comments".utf8) } }
— 31:04
And we would bake that default logic into Route ’s parse method. func parse(_ input: inout Parsers.Input) -> Output? { let original = input guard let output = self.parsers.parse(&input).map(self.transform), input.pathComponents.isEmpty, input.method == nil || input.method == "GET" else { input = original return nil } return output }
— 31:41
We would hope this would work, but we’ve removed another line from each of our routes, which means we have some buildBlock s we are missing. We even have a Route block that is completely empty: Route(AppRoute.home) { }
— 31:56
This is technically allowed with result builders. If we defined a buildBlock method with no arguments, this would compile. However, the syntax is a little strange as is, and it would be far nicer to omit the closure entirely for routes that have nothing left to parse: Route(AppRoute.home)
— 32:14
For this to compile we need another Route initializer that omits the block by defaulting to a parser that always succeeds with a void value. The library ships with such a parser, and it’s called Always . init( _ newOutput: NewOutput ) where Parsers == Always<Input, Void> { self.init({ newOutput }, parsers: { Always<Input, Void>(()) }) }
— 33:24
And now the first route is building, but we have a few more that aren’t, so let’s paste in the buildBlock overloads they require: static func buildBlock<P0, P1>( _ p0: P0, _ p1: P1 ) -> Parsers.SkipFirst<P0, P1> where P0: Parser, P1: Parser, P0.Output == Void { p0.take(p1) } static func buildBlock<P0, P1, P2>( _ p0: P0, _ p1: P1, _ p2: P2 ) -> Parsers.SkipSecond<Parsers.SkipFirst<P0, P1>, P2> where P0: Parser, P1: Parser, P2: Parser, P0.Output == Void, P2.Output == Void { p0.take(p1).skip(p2) }
— 33:59
And now everything builds and is super succinct! For the first time our router fits on a single screen.
— 34:15
But, we can take things even further. Now that we have builder syntax at our disposal we can imagine more ways to evolve the API to make it more succinct and more concise.
— 34:25
For example, in some of our routes we have repeated noise from needing to specify the Path parser for each path component we want to parse: Route(AppRoute.episode(id:)) { Path("episodes".utf8) Path(Int.parser()) } Route(AppRoute.episodeComments(id:)) { Path("episodes".utf8) Path(Int.parser()) Path("comments".utf8) }
— 34:42
What if instead we could make the Path parse take a builder closure, and then each parser listed in that builder context will be applied to the path components: Route(AppRoute.episode(id:)) { Method("GET") Path { "episodes".utf8 Int.parser() } } Route(AppRoute.episodeComments(id:)) { Method("GET") Path { "episodes".utf8 Int.parser() "comments".utf8 } }
— 35:11
Now there’s no need to repeat Path over and over.
— 5:35:15
In the future, once we start parsing more complex URLs that have query parameters, we could treat them similarly with a Query builder: Route(AppRoute.episodeComments(id:)) { Path { "episodes".utf8 Int.parser() "comments".utf8 } Query { Field("page", Int.parser()) Field("count", Int.parser()) } }
— 35:52
We aren’t going to implement this functionality right now, and instead leave it as an exercise for the viewer. So, let’s revert this work for now.
— 36:14
There’s another direction we could go to make our router more robust. Right now we have all of the routes listed in a single enum. We are lucky that there are only 5 routes, but in a large application there could be dozens or perhaps even over a hundred routes. We of course would not want to main an enum with dozens or hundreds of cases.
— 36:34
Instead, it would be better to create a deeply nested structure consisting of many enums, each only having a handful of cases. For example, our Route enum could be refactored into the following structure: enum AppRoute: Equatable { case home case contactUs case episodes(Episodes) } enum Episodes: Equatable { case root case episode(id: Int, Episode) } enum Episode: Equatable { case root case comments }
— 37:33
These enums represent all the same routes as the previous enum, but we now have a step-by-step way of specifying a route, such as the comments route for a particular episode: AppRoute.episodes(.episode(id: 1, .comments))
— 38:25
Once the route enum is refactored in this manner we can also refactor the router to mimic the deeply-nested structure. For the route enum we nested many smaller enums, and for the router we nest many smaller routers.
— 38:39
We could do this naively like so: let router = OneOf { Route(AppRoute.home) Route(AppRoute.contactUs) { Path("contact-us".utf8) } Route(AppRoute.episodes) { Path("episodes".utf8) OneOf { Route(Episodes.root) Route(Episodes.episode) { Path(Int.parser()) OneOf { Route(Episode.root) Route(Episode.comments) { Path("comments".utf8) } } } } } }
— 40:21
And this is already compiling! We now have a deeply-nested router that represents our deeply-nested enum.
— 41:12
Or we could break out smaller routers that focus on the routing for a single domain, such as the episodes domain or the episode domain: let router = OneOf { Route(AppRoute.home) Route(AppRoute.contactUs) { Path("contact-us".utf8) } Route(AppRoute.episodes) { Path("episodes".utf8) episodesRouter } } let episodesRouter = OneOf { Route(Episodes.root) Route(Episodes.episode) { Path(Int.parser()) episodeRouter } } let episodeRouter = OneOf { Route(Episode.root) Route(Episode.comments) { Path("comments".utf8) } }
— 41:51
If that wasn’t benefit enough, refactoring our router in this way has improved the performance of our routing! By nesting our router, we eliminate the duplication of parsing the "episodes" path component and even the integer path component from our episode routes. Previously, when a route failed, this work would be repeated for each subsequent route. But now, if "episodes" fails it will bail out of the entire episodesRouter and go onto the next one.
— 42:45
And if we were working in an iOS application we wanted to modularize, a topic we recently dove deep into ( part 1 , part 2 ), we could further extract out these routers into their respective feature modules. This would allow you to hyper-focus on a specific feature’s deep linking logic without caring about how the greater application does its deep linking. Conclusion
— 43:23
This concludes our series of episodes on bringing results builders to our parser library. We are also releasing a new version of the library so that everyone can start using this style of parsing immediately.
— 43:34
We still have big plans for parsers this year, but that’ll have to wait until next time. References Collection: Parsing Brandon Williams & Stephen Celis Note Parsing is a surprisingly ubiquitous problem in programming. Every time we construct an integer or a URL from a string, we are technically doing parsing. After demonstrating the many types of parsing that Apple gives us access to, we will take a step back and define the essence of parsing in a single type. That type supports many wonderful types of compositions, and allows us to break large, complex parsing problems into small, understandable units. https://www.pointfree.co/collections/parsing Swift Parsing Brandon Williams & Stephen Celis • Dec 21, 2021 A library for turning nebulous data into well-structured data, with a focus on composition, performance, generality, and invertibility. https://github.com/pointfreeco/swift-parsing Declarative String Processing Alex Alonso, Nate Cook, Michael Ilseman, Kyle Macomber, Becca Royal-Gordon, Tim Vermeulen, and Richard Wei • Sep 29, 2021 The Swift core team’s proposal and experimental repository for declarative string processing, which includes result builder syntax for creating regular expressions, and inspired us to explore result builders for parsing. https://github.com/apple/swift-experimental-string-processing The Many Faces of Map Brandon Williams & Stephen Celis • Apr 23, 2018 Note Why does the map function appear in every programming language supporting “functional” concepts? And why does Swift have two map functions? We will answer these questions and show that map has many universal properties, and is in some sense unique. https://www.pointfree.co/episodes/ep13-the-many-faces-of-map Downloads Sample code 0175-parser-builders-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 .