Video #174: Parser Builders: The Solution
Episode: Video #174 Date: Jan 17, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep174-parser-builders-the-solution

Description
Let’s begin to layer result builder syntax on top of parsing. To get our feet wet, we will build a toy result builder from scratch. Then, we will dive much deeper to apply what we learn to parsers.
Video
Cloudflare Stream video ID: cebcc1603a0a693cc000c4f76a335698 Local file: video_174_parser-builders-the-solution.mp4 *(download with --video 174)*
References
- Discussions
- Swift Parsing
- Declarative String Processing
- 0174-parser-builders-pt2
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
We think this generated file is just a stopgap until Swift gains the full power of variadic generics, but either way, it got us wondering whether generated source code would be a good fit for our parser library, and then we could just rip them out once variadic generics arrive in Swift. We were most worried about compile times, but we do know that the Swift compiler team has greatly improved the performance of result builders. Our early experiments were extremely promising, so we went all in and we think it is the future of the library.
— 0:36
Before diving in, let’s first discuss result builders from first principles so that we all know how to leverage them to their full potential, and then we will see how these ideas apply to parsers. We will show that result builders go far beyond just simple ergonomics for our parsing library. They unlock all new forms of APIs that can be really, really powerful.
— 0:57
So, let’s get started. Result builders
— 1:00
Everyone watching this episode has most likely interacted with a result builder by virtue of the fact that SwiftUI heavily leverages them for its declarative views. If you’ve ever written code that looks like it’s creating a deep nested hierarchy of types with a whole bunch of closures, you were using result builders: import SwiftUI let view = Group { ForEach(1...10, id: \.self) { index in VStack { Text("\(Number)") Text("\(index)") } } }
— 1:18
This is the most common way to interact with result builders, but there’s another usage of result builders that can come up, and it pulls back the curtain a bit on what is happening behind the scenes.
— 1:28
If you’ve ever wanted to create your own custom SwiftUI component for which you want the callers of your API to be able to leverage result builder syntax, then you needed to sprinkle in some @ViewBuilder annotations. For example, we could create a custom view that represents a common UI pattern we encounter in our application repeatedly. This custom view would take in other views as arguments and then arrange them in some specific way: struct Template<Title: View, Content: View>: View { let title: Title let content: Content var body: some View { VStack { self.title self.content } } }
— 2:09
Defined like this we would pass along views directly to Template ’s initializer: Template(title: Text("Hi"), content: Text("Welcome!"))
— 2:23
But, if we annotate the instance variables with @ViewBuilder like so: @ViewBuilder let title: Title @ViewBuilder let content: Content
— 2:32
We instantly unlock the ability to use builder syntax: Template { Text("Hi") } content: { Text("Welcome!") }
— 2:45
Notice that the instance variables are declared as plain Title and Content values, yet somehow the initializer is taking closures. This must be due to some compiler magic being performed behind the scenes, which makes using result builders a little simpler to use.
— 3:03
And we can leverage everything that builders has to offer in this closure, including putting in more views, using conditionals, and more: Template { if true { Text("Hi") } else { Text("Bye") } Spacer() } content: { Text("Welcome!") }
— 3:19
And if you wanted to provide a custom initializer you would move the @ViewBuilder annotation to appear before the name of the argument: init( @ViewBuilder title: Title, @ViewBuilder content: Content ) { self.title = title self.content = content } Result builder attribute ‘ViewBuilder’ can only be applied to a parameter of function type
— 3:36
But, now we have to be more explicit and say that our @ViewBuilder arguments are closures that take no arguments, and we will eagerly evaluate them so they don’t even need to be escaping closures: init( @ViewBuilder title: () -> Title, @ViewBuilder content: () -> Content ) { self.title = title() self.content = content() }
— 3:53
This @ViewBuilder annotation is an example of a result builder. We can even hop over to the generated header file for SwiftUI and see that ViewBuilder is nothing but a plain struct, but with a @resultBuilder annotation added to its declaration: @resultBuilder public struct ViewBuilder { … }
— 4:08
The fact that this type is marked with @resultBuilder is what allows us to use the @ViewBuilder syntax in our Template view. For the most part, the ViewBuilder acts as a namespace to house a bunch of static methods. It seems like it would even be possible to use an enum for the namespace instead of a struct, but maybe Apple has some specific reason for reaching for a struct in this case.
— 4:29
So, let’s create a builder of our own. We’re not going to jump straight to parser builders, as they have quite a bit of complexity. Instead, let’s get our feet wet with a toy builder so that we can understand how to create builders.
— 4:42
We are going to create a string builder, that allows you to builder strings using syntax like this: String { "Hello" " " "World" } // "Hello World" The entry point into the builder syntax will be a custom initializer on String that we define, and it simply allows you to specify a list of strings, and it will concatenate them together.
— 5:06
Now, that may not seem too exciting, and it certainly doesn’t seem any better than using a regular string. But, by using builder syntax we can intermingle Swift constructs inside the builder to add logic to how we build the string.
— 5:21
For example, we could perform conditional logic: let useSpace = true String { "Hello" if useSpace { " " } else { "-" } "World" }
— 5:37
We could perform a for loop in order to concatenate many strings: let useSpace = true String { "Hello" if useSpace { " " } else { "-" } "World" for _ in 1...10 { "!" } }
— 6:01
And this is only scratching the surface of what’s possible.
— 6:04
So, let’s implement a string builder that will make all of this code compile. We can start by creating a new enum namespace with the @resultBuilder attribute applied: @resultBuilder enum StringBuilder { } Result builder must provide at least one static ‘buildBlock’ method
— 6:19
Doing just that already gives us a compiler error telling us that we must provide at least on buildBlock static method inside our StringBuilder type.
— 6:26
These static “build” methods are what provide the magic of result builders behind the scene. For each kind of syntax you can perform inside a result builder there is a corresponding static “build” method you need to implement.
— 6:38
If we simply type “build” inside StringBuilder we will see autocomplete of a bunch of options we can implement.
— 6:44
Each of these build methods corresponds to some kind of syntax you can use in a builder context. If we implement enough of these static methods we will eventually get our theoretical string builder syntax compiling.
— 6:55
For example, if you want to just be able to list a bunch of things in the builder closure, like we have in the String initializer, then you need to implement the buildBlock static method. If we autocomplete buildBlock we will see it has the following signature: @resultBuilder enum StringBuilder { static func buildBlock(_ components: <#Component#>...) -> <#Component#> { <#code#> } }
— 7:06
This function takes a variadic list of values and needs to return a value. The variadic list of values represents the things listed in the builder closure, and the return value is what is the final result that is built.
— 7:17
For our string builder, all it does is concatenate everything together, so we can implement it like so: @resultBuilder enum StringBuilder { static func buildBlock(_ components: String...) -> String { components.joined() } }
— 7:31
And already with that we should be able to form some very simple builder expressions. However, in order to do so we need an entry point into an actual builder context. That’s what the String { } expression was doing in our pseudo code. Theoretically it’s just an initializer on the String type that takes a string builder as an argument.
— 7:48
We can implement this initializer like so: extension String { init(@StringBuilder build: () -> String) { self = build() } }
— 8:04
And finally we can take our builder for a spin, though we need to add an explicit build argument to disambiguate it from all the other string initializers out there: String(build: { "Hello" " " "World" }) // "Hello World" Nothing too impressive so far, but let’s keep adding to it.
— 8:29
If we try to introduce an if statement to the builder: let useSpace = true String(build: { "Hello" if useSpace { " " } "World" }) closure containing control flow statement cannot be used with result builder ‘StringBuilder’ add ‘buildOptional(_:)’ to the result builder ‘StringBuilder’ to add support for ‘if’ statements without an ‘else’
— 8:34
We get a compiler error letting us know that we can’t use if statements in the result builder. To enable this functionality we need to implement the buildOptional static method: static func buildOptional(_ component: String?) -> String { }
— 8:52
The optional string input represents the two states of the if statement: if the condition is true, then we have a string, otherwise we have nothing. In the context of a string builder, if the condition is false we simply do not want to concatenate anything to the final string, so we can coalesce this input to an empty string: static func buildOptional(_ component: String?) -> String { component ?? "" }
— 9:15
And just like that the code is compiling again and we have built a more complex string: let useSpace = true String(build: { "Hello" if useSpace { " " } "World" }) // "Hello World"
— 9:22
And if we flip useSpace to false we will get a string with no space because we coalesced the optional string into an empty string: let useSpace = false String(build: { "Hello" if useSpace { " " } "World" }) // "HelloWorld"
— 9:27
If we add an else statement to this if: let useSpace = true String(build: { "Hello" if useSpace { " " } else { "-" } "World" }) closure containing control flow statement cannot be used with result builder ’StringBuilder’ note: add ‘buildEither(first:)’ and ‘buildEither(second:)’ to the result builder ‘StringBuilder’ to add support for ‘if’-‘else’ and ’switch’
— 9:29
We get another compiler error telling us we can’t do that. That’s because we need to further implement two buildEither methods that allow us to decide how to handle each branch of the if/else statement, and as a bonus it also works for switch statements.
— 9:49
These methods are even easier to implement. Since the else statement acts as a catch-all for the conditional we are guaranteed to always produce a string from one of the logical branches. As soon as one branch evaluates to true we can simply return that string: static func buildEither(first component: String) -> String { component } static func buildEither(second component: String) -> String { component }
— 10:07
And now this compiles: let useSpace = false String(build: { "Hello" if useSpace { " " } else { "-" } "World" }) // "Hello-World"
— 10:14
In fact, we can now even use switch statements in the builder: let useSpace = false String(build: { "Hello" switch useSpace { case true: " " case false: "-" } "World" })
— 10:32
In more complex result builders we may want to construct very different things in each buildEither method, but this string builder example doesn’t need such flexibility.
— 10:39
And finally, if we try to put in a for loop: let useSpace = false String(build: { "Hello" switch useSpace { case true: " " case false: "-" } "World" for _ in 1...10 { "!" } }) closure containing control flow statement cannot be used with result builder ’StringBuilder’ note: add ‘buildArray(_:)’ to the result builder ‘StringBuilder’ to add support for ‘for’..‘in’ loops
— 10:43
We get a compiler error that we can’t do that. In order to support for loops we need to further implement the buildArray static method: static func buildArray(_ components: [String]) -> String { }
— 10:53
The array of components handed to this method are simply all the values produced from the for loop in the builder, and then it is our job to turn all of that into a single string.
— 10:59
To do that we can just join the strings: static func buildArray(_ components: [String]) -> String { components.joined() }
— 11:04
And now this compiles: let useSpace = false String(build: { "Hello" switch useSpace { case true: " " case false: "-" } "World" for _ in 1...10 { "!" } }) // "Hello-World!!!!!!!!!!!" Parser builders
— 11:13
So we now understand the basics of result builders, and how we can support more Swift constructs inside a builder by implementing more of the static “build” methods. But, so far our result builder isn’t very interesting. We can only build up a simple string.
— 11:28
Let’s now turn our attention to parser builders.
— 11:31
Unfortunately, there’s no real easy way to lean into parser builders. We’re going to throw ourselves into the deep end pretty quickly because the most fundamental builder syntax we want to unlock is being able to list a few parsers, one after the other, in order to successively run each parser and collect the results into a tuple.
— 11:49
Let’s use the “user” parser from earlier in order to figure out how to implement the parser builder syntax.
— 12:00
Previously we wanted to parse an integer, then a comma, then everything up to the comma, then another comma, and then finally the role. Let’s comment out the old take/skip style parser for the user, and bring back the builder syntax that we theorized: let user = Parse(User.init(id:name:role:)) { Int.parser() "," Prefix { $0 != "," }.map(String.init) "," role }
— 12:27
In order to make this work we are going to need a top-level Parse type to act as the entry point of parser builder syntax, and we are going to need an implementation of the buildBlock static method that takes 5 parsers and discards the void values produced by the commas.
— 12:48
Let’s see what it takes to get this compiling. We’ll start by getting our parser builder namespace into place: @resultBuilder enum ParserBuilder { }
— 13:03
We’ll need to fill in some build methods in this type soon, but before then let’s take care of this top-level Parse type that we are using as an entry point into parser builder syntax.
— 13:13
This is a new type that that doesn’t currently exist in the library, and you can kind of think of it as the Group view in SwiftUI, which gives you quick access to view builder syntax: Group { Text("Hi") Button("Bye") { } }
— 13:30
We of course don’t know what this Group type looks like on the inside since SwiftUI is closed source, but it’s probably nothing special. Most likely it just exposes an initializer that takes a view builder, and then holds onto the evaluated view as an instance variable so that it can be returned from the body property: import SwiftUI struct _Group<Content>: View where Content: View { let content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { self.content } }
— 14:21
The Parse type will be very similar. It’s a whole new type whose only purpose is to expose an initializer with a parser builder closure so that it can act as an entry point into builder syntax: struct Parse<Parsers>: Parser where Parsers: Parser { let parsers: Parsers init(@ParserBuilder parsers: () -> Parsers) { self.parsers = parsers() } func parse(_ input: inout Parsers.Input) -> Parsers.Output? { self.parsers.parse(&input) } }
— 15:15
This is close to the final form we want, but there’s one additional feature we want to add. We want the ability to specify a function when opening up the builder syntax which will automatically be applied as a mapping function after parsing occurs: let user = Parse(User.init(id:name:role:)) { Int.parser() "," Prefix { $0 != "," }.map(String.init) "," role }
— 15:36
To support this we need Parse to allow us to specify a transform function for turning the parser’s output into a new kind of output. init( _ transform: (Parsers.Output) -> NewOutput, @ParserBuilder parsers: () -> Parsers ) { … }
— 15:54
Which means introducing a new generic to the parser: struct Parse<Parsers, NewOutput>: Parser where Parsers: Parser { … }
— 15:59
Holding onto the transform function: let transform: (Parsers.Output) -> NewOutput
— 16:05
Which means assigning an escaping version in the initializer: init( _ transform: @escaping (Parsers.Output) -> NewOutput, @ParserBuilder parsers: () -> Parsers ) { self.transform = transform self.parsers = parsers() }
— 16:13
And we can update parse to apply this transformation: func parse(_ input: inout Parsers.Input) -> NewOutput? { self.parsers.parse(&input).map(self.transform) }
— 16:21
This is now compiling, but to allow Parse to be used without a transform function, which will parse things into a tuple instead, we should introduce another constrained initializer, which does not take a transform function: init( @ParserBuilder parsers: () -> Parsers ) where Parsers.Output == NewOutput { self.transform = { $0 } self.parsers = parsers() }
— 17:12
We’re getting closer to getting everything to compile, but we have more work to do. Next we need to actually implement our first “build” static method on the ParserBuilder type. It will be a buildBlock method because we want to simply list out a few parsers inside the Parse { } closure.
— 17:29
We can have Xcode stub in a buildBlock method. @resultBuilder enum ParserBuilder { static func buildBlock( _ components: <#Component#>... ) -> <#Component#> { <#code#> } }
— 17:31
But we can’t simply specify a variadic list of the Parser type: static func buildBlock( _ components: Parser... ) -> <#Component#> { <#code#> }
— 17:40
Because Parser is a protocol with associated types. Instead, we need to introduce generics representing parsers. static func buildBlock<P>( _ components: P... ) -> <#Component#> { <#code#> }
— 17:51
But even this isn’t right, because each parser can be a completely different type. For example, in the user parser we defined we have an Int.parser() , a Prefix parser, a custom role parser, and a couple string parsers for the commas, so 4 of them have completely different types.
— 18:14
This means we need to introduce 5 generics for this static method, and constrain them all to be parsers: static func buildBlock<P0, P1, P2, P3, P4>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4 ) -> <#Component#> where P0: Parser, P1: Parser, P2: Parser, P3: Parser, P4: Parser { <#code#> }
— 18:49
Further, these generics can’t just be any type of parser. They must all work on the same type of input in order for us to be able to run one parser after another: static func buildBlock<P0, P1, P2, P3, P4>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4 ) where P0: Parser, P1: Parser, P2: Parser, P3: Parser, P4: Parser, P0.Input == P1.Input, P1.Input == P2.Input, P2.Input == P3.Input, P3.Input == P4.Input -> <#Component#> { <#code#> }
— 19:18
We want to return a parser from the buildBlock method that takes care of running all 5 of these parsers, but we only want to take the outputs of the first, third and fifth parser. The second and fourth parsers output void values, and so we can safely discard them.
— 19:35
To construct such a parser we can use use the .take and .skip operators: static func buildBlock<P0, P1, P2, P3, P4>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4 ) -> <#Component#> where P0: Parser, P1: Parser, P2: Parser, P3: Parser, P4: Parser, P0.Input == P1.Input, P1.Input == P2.Input, P2.Input == P3.Input, P3.Input == P4.Input { p0.skip(p1).take(p2).skip(p3).take(p4) }
— 19:46
And now all we need to do is figure out the type of this parser so that we can update the return type of the function. We can assign the parser to a temporary variable so that we can inspect the type, which allows us to finally complete the method: static func buildBlock<P0, P1, P2, P3, P4>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4 ) -> Parsers.Take3< Parsers.SkipSecond< Parsers.Take2<Parsers.SkipSecond<P0, P1>, P2>, P3 >, P0.Output, P2.Output, P4 > where P0: Parser, P1: Parser, P2: Parser, P3: Parser, P4: Parser, P0.Input == P1.Input, P1.Input == P2.Input, P2.Input == P3.Input, P3.Input == P4.Input { p0.skip(p1).take(p2).skip(p3).take(p4) }
— 20:02
This now compiles, but we do have a few warnings. It seems that Swift can infer that the inputs of all the parsers must be equal by virtue of the fact that we are using Take2 , Take3 and SkipSecond types: static func buildBlock<P0, P1, P2, P3, P4>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4 ) -> Parsers.Take3< Parsers.SkipSecond< Parsers.Take2<Parsers.SkipSecond<P0, P1>, P2>, P3 >, P0.Output, P2.Output, P4 > where P0: Parser, P1: Parser, P2: Parser, P3: Parser, P4: Parser { p0.skip(p1).take(p2).skip(p3).take(p4) }
— 20:21
Now the warnings go away, and everything compiles. And I mean everything.
— 20:37
Our theoretical parser builder syntax is now compiling, and is parsing exactly as it did before: let user = Parse(User.init(id:name:role:)) { Int.parser() "," Prefix { $0 != "," }.map(String.init) "," role }
— 21:21
This is really awesome. This is a much better syntax for expressing parsers. We no longer have to worry about .skip and .take operators, and instead the Void values will automatically be discarded.
— 21:31
Now, this is technically working, but there is one strange thing. The point of the buildBlock we implemented is to run all 5 parsers, one after another, and discard the void values from the second and forth parser. However, there is no representation of the fact that we expect the second and fourth parsers to be void parsers. In fact, we can even plug non-void parsers into those arguments, and it will still compile: let user = Parse(User.init(id:name:role:)) { Int.parser() Bool.parser() Prefix { $0 != "," }.map(String.init) Bool.parser() role }
— 22:07
This is going to become problematic when later down the road we need another overload of buildBlock that works on five arguments with a different combination of void and non-void values. Like, say, one that needs to do a skip, take, take, take, skip. Such a buildBlock method would have the exact same signature as our current one, and so would have no way of knowing which one to choose.
— 22:43
We need to give Swift more information so that it knows which one to choose, and we can do that by further constraining the method so that we specify exactly which parsers we expect to be void: static func buildBlock<P0, P1, P2, P3, P4>( _ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4 ) -> Parsers.Take3< Parsers.SkipSecond< Parsers.Take2<Parsers.SkipSecond<P0, P1>, P2>, P3 >, P0.Output, P2.Output P4 > where P0: Parser, P1: Parser, P2: Parser, P3: Parser, P4: Parser, P1.Output == Void, P3.Output == Void { p0.skip(p1).take(p2).skip(p3).take(p4) } Static method ‘buildBlock’ requires the types ‘Bool’ and ‘Void’ be equivalent
— 23:04
Now the parser fails to build because we aren’t supplying 5 parsers such that the second and fourth are Void parsers. If we revert back to what we had before it will compile again, and in the future as we add more overloads we will not run into ambiguity problems.
— 23:26
So, we’ve now got a single buildBlock method that allows us to parse 5 things when we specifically want to discard the output of the 2nd and 4th parser. We of course will need a ton more of these buildBlock methods, one for each number of parsers we want to run at once, as well as one for each combination of those parsers return void or non-void output. However, we’re not going to write those overloads now, and instead we will introduce new buildBlock methods as we need them, and really in the future we should make a tool that generates all of that code like Apple is doing in their string processing project, and hopefully soon we won’t even need to generate that code because Swift should have variadic generics. @OneOfBuilder
— 24:08
We’ve now converted most of the users parser into the new builder syntax, except for the role parser. Currently it is using the .orElse operator in order to express the idea that we want to try multiple parsers on the input, and take the first one that succeeds: let role = "admin".map { Role.admin } .orElse("guest".map { Role.guest }) .orElse("member".map { Role.member })
— 24:35
This parser is quite noisy due to the repeated use of .orElse , and the first parser is given more prominence than the others because it is not surrounded by the orElse .
— 24:46
Ideally we could use parser builders to remove the noise and flatten this a bit: let role = OneOf { "admin".map { Role.admin } "guest".map { Role.guest } "member".map { Role.member } }
— 24:58
This doesn’t yet build because there is no OneOf type defined at the top-level of the library’s module scope. We do have a OneOf type in the library, but it’s currently namespaced because it’s not a type you typically write directly. Instead, you chain the .orElse operator as many times as you want, and under the hood this builds up a nested OneOf type. This is similar to how the Combine framework is designed, where many publishers reside in the Publishers namespace and are only constructed via operators, such as Publishers.Map .
— 25:49
However, even if we fully qualify the type, it still does not compile: let role = Parsers.OneOf { "admin".map { Role.admin } "guest".map { Role.guest } "member".map { Role.member } }
— 26:00
This is because there’s no initializer on the OneOf type that accepts a parser builder, which is what we are trying to do here.
— 26:07
We can add one, but we have to decide what kind of type we want to return from the parser builder: extension Parsers.OneOf { init(@ParserBuilder build: () -> ???) { } }
— 26:20
The type returned by the build closure must match something that is returned from a “build” static method defined on the ParserBuilder type. After all, everything that is written inside a builder closure gets rewritten into actual types by invoking the various build methods under the hood.
— 26:35
However, currently we only have have two “build” methods, one that takes a single parser, and the other takes 5 parsers. Seems like we need another overload of buildBlock that only takes 3 parsers, since that’s how many cases of the Role enum we need to handle, and instead of constructing take/skip parsers under the hood, it should use the .orElse operator: extension ParserBuilder { static func buildBlock<P0, P1, P2>( _ p0: P0, _ p1: P1, _ p2: P2 ) -> Parsers.OneOf<Parsers.OneOf<P0, P1>, P2> { p0.orElse(p1).orElse(p2) } }
— 27:41
Then we could implement the OneOf initializer by requiring the parser builder to return an instance of OneOf , which is now possible thanks to our new buildBlock overload: extension Parsers.OneOf { public init(@ParserBuilder build: () -> Self) { self = build() } }
— 27:55
And just like that, our role parser is compiling! let role = Parsers.OneOf { "admin".map { Role.admin } "guest".map { Role.guest } "member".map { Role.member } }
— 28:04
To get things even closer to our theoretical syntax, we can even add a type alias to break OneOf out of the Parsers namespace: typealias OneOf = Parsers.OneOf let role = OneOf { "admin".map { Role.admin } "guest".map { Role.guest } "member".map { Role.member } }
— 28:27
While everything is building and running and working exactly as it did before, unfortunately this isn’t going to work in the long run. We actually have an ambiguity problem lurking in the shadows, we just can’t see it yet because we haven’t written enough parsers with the new builder syntax.
— 28:51
For example, what if we swapped our role parser’s OneOf out for a Parse ? let role = Parse { "admin".map { Role.admin } "guest".map { Role.guest } "member".map { Role.member } }
— 28:56
We’d want this to be a parser of a tuple of three roles. However, because we have the buildBlock implemented that takes three parsers and returns a OneOf parser, it’s still returning a OneOf .
— 29:20
Maybe we can define a whole new buildBlock that does what we want, which is to use .take operations instead of .orElse operations: static func buildBlock<P0, P1, P2>( _ p0: P0, _ p1: P1, _ p2: P2 ) -> Parsers.Take3<Parsers.Take2<P0, P1>, P0.Output, P1.Output, P2> where P0: Parser, P1: Parser, P2: Parser { p0.take(p1).take(p2) }
— 29:42
But that still doesn’t work. Our parser of three roles is still interpreted by Swift as being a parser that produces a single one. Swift is probably choosing the OneOf overload because it is more specific than the Take overload, as it requires all of the parsers’ inputs to be equal and outputs to be equal, whereas Take only requires the inputs to be equal.
— 30:00
The way to work around this issue is to not overload @ParserBuilder to work with both Take parsers and OneOf parsers. Parser builders are already extremely overloaded with just the take parsers, and so there’s no need to throw even more overloads into the mix.
— 30:14
What we can do instead is introduce another result builder, one specifically for building OneOf parsers. We will call this a OneOfBuilder , and for now it can have a single buildBlock that takes three parsers, and in fact we can just cut-and-paste the one we created a moment ago: @resultBuilder enum OneOfBuilder { static func buildBlock<P0, P1, P2>( _ p0: P0, _ p1: P1, _ p2: P2 ) -> Parsers.OneOf<Parsers.OneOf<P0, P1>, P2> { p0.orElse(p1).orElse(p2) } }
— 30:36
Then the initializer on OneOf can change to use a @OneOfBuilder instead of a @ParserBuilder : extension Parsers.OneOf { public init(@OneOfBuilder build: () -> Self) { self = build() } }
— 30:47
And with just those few changes our user parser no longer compiles, because its role parser is now outputting a tuple of roles instead of the single role it expects. And if we change the role parser back to use OneOf instead, everything compiles.
— 31:08
So we have introduced two different result builders, and we’ve implemented a few buildBlock methods that now give us the ability to write our role and user parsers quite succinctly: let role = OneOf { "admin".map { Role.admin } "guest".map { Role.guest } "member".map { Role.member } } let user = Parse(User.init(id:name:role:)) { Int.parser() "," Prefix { $0 != "," }.map(String.init) "," role }
— 31:31
This is already a pretty huge win, but things give even better. Reimagined APIs
— 31:47
Now that we have the basics of a parser builder in place, we can start having fun with it. We can start to explore new styles of parser APIs based on the new affordances that result builders give us.
— 32:07
For example, the Many parser is still being used in the old style: let users = Many(user, separator: "\n")
— 32:21
We theorized an alternative API that used result builders, but it’s commented out right now: // let users = Many { user } separator: { "\n" }
— 32:32
Let’s bring back this code and see what it takes to make it compile.
— 32:35
We can introduce an initializer on Many that takes parser builders for the main parser as well as the separator parser, but now with a new argument label: extension Many where Result == [Element.Output] { init( @ParserBuilder _ element: () -> Element, @ParserBuilder separator: () -> Separator ) { self.init(element(), separator: separator()) } }
— 34:06
Now we can write our users parser in this more fluent style: let users = Many { user } separator: { "\n" }
— 34:09
Unfortunately this doesn’t compile yet: Missing arguments for parameters #2, #3 in call
— 34:11
This is telling us that it expected us to provide three arguments to this parser builder closure, but we only provided a single one. And that’s because we only have buildBlock methods defined for 3 arguments and 5 arguments.
— 34:28
We now need to define another overload of buildBlock that works for just a single parser, which is straightforward because it doesn’t actually have to do anything fancy: extension ParserBuilder { static func buildBlock<P0>(_ p0: P0) -> P0 where P0: Parser { p0 } }
— 34:53
Now everything is compiling, including our new Many parser syntax, and everything works exactly as it did before.
— 35:03
If we wanted to, we could even get rid of all the little parsers that we are defining and just pile everything into one big parser: let users = Many { Parse(User.init(id:name:role:)) { Int.parser() "," Prefix { $0 != "," }.map(String.init) "," OneOf { "admin".map { Role.admin } "guest".map { Role.guest } "member".map { Role.member } } } } separator: { "\n" }
— 35:31
The parser builder syntax makes this much more readable than it would have been with the take / skip / orElse operators. Next time: what’s the point?
— 36:08
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.
— 36:18
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?
— 36:42
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 cleanup noise and have the parses tell a more concise story, and gives us a chance to create new parsing tools that leverage builder syntax.
— 37:05
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. 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 Downloads Sample code 0174-parser-builders-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 .