EP 180 · Invertible Parsing · Mar 7, 2022 ·Members

Video #180: Invertible Parsing: The Solution, Part 2

smart_display

Loading stream…

Video #180: Invertible Parsing: The Solution, Part 2

Episode: Video #180 Date: Mar 7, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep180-invertible-parsing-the-solution-part-2

Episode thumbnail

Description

We will chip away at more and more parser printer conformances, some of which will truly stretch our brains, but we will finally turn our complex user CSV parser into a printer!

Video

Cloudflare Stream video ID: 2b60777218a6e003bb750c22818b2cc6 Local file: video_180_invertible-parsing-the-solution-part-2.mp4 *(download with --video 180)*

References

Transcript

0:05

We are getting very close to having our first moderately complex printer.

0:20

We are now on the precipice of something really amazing. Let’s take a look at the field parser, which is responsible for first trying to parse a quoted field, and if that fails it just parses a regular field up until the next comma. Map parser-printing

0:34

It has the following definition, which alternates each parse in a OneOf block and then map s the result into a String : let field = OneOf { quotedField Prefix { $0 != "," } } .map(String.init)

0:34

Both quotedField and Prefix are printers, so we would hope that we could call print on field : input = "" field.print("Blob Jr.", to: &input) Value of type ‘Parsers.Map<OneOf<Parsers.OneOf2<Parse<Parsers.ZipVOV<String, Prefix<Substring>, String>>, Prefix<Substring>>>, String>’ has no member ‘print’

1:05

But we can’t, because some of the parsers involved still aren’t printers, including OneOf , OneOf2 , and the Map parser.

1:23

Let’s start with Map . We can get a stub in place to see what it takes to turn into a printer: extension Parsers.Map: Printer { func print(_ output: NewOutput, to input: inout Upstream.Input) throws { <#code#> } }

1:40

Just like the other printer combinators we’ve encountered, if we have any hope of turning this into a printer we are going to need the parser it wraps to also be a printer: extension Parsers.Map: Printer where Upstream: Printer { … }

1:54

So we need to figure out to print a NewOutput into an Upstream.Input . We have access to self.upstream , which we now know is a printer, so perhaps we could use it to print: extension Parsers.Map: Printer where Upstream: Printer { func print(_ output: NewOutput, to input: inout Upstream.Input) throws { self.upstream.print(<#Upstream.Output#>, to: &<#Upstream.Input#>) } }

2:13

However, these types don’t match up. Upstream’s print wants an Upstream.Output , but all we have access to is a NewOutput .

2:20

We do have access to one more piece of information, which is the transform function that is used with the .map operator: public let transform: (Upstream.Output) -> NewOutput

2:27

It can transform Upstream.Output values to NewOutput values, which is exactly what we needed when parsing: public func parse(_ input: inout Upstream.Input) rethrows -> NewOutput { self.transform(try self.upstream.parse(&input)) }

2:32

But remember, printing is supposed to be the reverse of parsing, and all this transformation goes in the wrong direction. It can transform Upstream.Output into NewOutput , but NewOutput is what we have at the time of printing. Not Upstream.Output .

2:44

This print method is actually impossible to implement as it stands now. We simply do not have enough information at our disposal to transform NewOutput into Upstream.Output so that we can then feed that to self.upstream.print . It turns out the humble map operation that we know and love is just not printer-friendly, at least not in its current incarnation.

3:04

There is something we can do to bring the map operation into the world of printers, but it requires us to stretch our minds in some really uncomfortable ways. We want to show off some of the powers of printers before going down that road, so we are going to put a pause on this operation.

3:18

So, let’s comment out this Printer conformance for Parsers.Map , and let’s drop the .map operation from the little parser we are concentrating on and kick it over to the parser that uses it: let field = OneOf { quotedField Prefix { $0 != "," } } // .map(String.init) … let user = Parse(User.init(id:name:admin:)) { … field.map(String.init) … } OneOf parser-printing

3:30

And now where we attempt to print: field.print("Blob Jr.") Value of type ‘OneOf<Parsers.OneOf2<Parse<Parsers.ZipVOV<String, Prefix<Substring>, String>>, Prefix<Substring>>>’ has no member ‘print’ We’re down to OneOf and OneOf2 needing to be printers.

3:35

The OneOf type is very similar to the Parse type, in that it is simply an entry point into a OneOfBuilder context: public struct OneOf<Parsers: Parser>: Parser { public let parsers: Parsers @inlinable public init(@OneOfBuilder _ build: () -> Parsers) { self.parsers = build() } @inlinable public func parse(_ input: inout Parsers.Input) -> Parsers.Output? { self.parsers.parse(&input) } }

3:44

It simply wraps an existing parser, calls out to that parser for its own parse method, and exposes an initializer for listing a bunch of parsers that we want to run, one after another, until we find one that succeeds.

3:57

Making this into a printer is very straightforward. We can just repeat what we did for Parse , which is to make it a Printer when its wrapped type is a printer, and then call out to that printer in the print method: extension OneOf: Printer where Parsers: Printer { func print(_ output: Parsers.Output, to input: inout Parsers.Input) throws { try self.parsers.print(output, to: &input) } }

4:16

Now call print on the field has a new error message: field.print("Blob Jr.") Referencing instance method ‘print’ on ‘OneOf’ requires that ‘Parsers.OneOf2<Parse<Parsers.ZipVOV<String, Prefix<Substring>, String>>, Prefix<Substring>>’ conform to ‘Printer’

4:19

Just as the Parse type wrapped one of those Zip types, such as ZipVOV , the OneOf type also wraps another type, in this case a OneOf2 since we are using two parsers in the OneOf closure.

4:21

This OneOf2 type is code generated just like the Zip types because we need a version of this type for every arity of buildBlock we support, which for our library is 10. We can hop to its definition to see what it does: extension Parsers { public struct OneOf2<P0: Parser, P1: Parser>: Parser where P0.Input == P1.Input, P0.Output == P1.Output { public let p0: P0, p1: P1 @inlinable public init(_ p0: P0, _ p1: P1) { self.p0 = p0 self.p1 = p1 } @inlinable public func parse( _ input: inout P0.Input ) rethrows -> P0.Output { let original = input do { return try self.p0.parse(&input) } catch let e0 { do { input = original; return try self.p1.parse(&input) } catch let e1 { throw ParsingError.manyFailed( [e0, e1], at: input ) } } } } }

4:36

This type looks a little complicated, but it’s actually doing something quite simple. It holds onto two parsers, both of which must have the same type of input and output. The parse method simply tries each parser, and returns the output of the first that succeeds. If all fail, then it reverts the input back to where it started and returns a new error that bundles up all the failures into a single one.

5:01

Let’s see if we can turn OneOf2 into a printer. We’ll start by getting a stub of an implementation in place: extension Parsers.OneOf2: Printer { func print(_ output: P0.Output, to input: inout P0.Input) throws { <#code#> } }

5:16

And just as with our other printer combinators we are going to assume that the parsers used on the inside to also be printers: extension Parsers.OneOf2: Printer where P0: Printer, P1: Printer { … }

5:27

Now we have to figure out how to print a P0.Output into a P0.Input . Remember that we are only dealing with P0 ’s input and output because both parsers have the same input and output in order to be used with a OneOf .

5:42

So, how can we do this? Well, we have access to self.p0 and self.p1 , both of which are printers, and so perhaps we can run each printer and just early out on the first one that succeeds: extension Parsers.OneOf2: Printer where P0: Printer, P1: Printer { func print(_ output: P0.Output, to input: inout P0.Input) throws { let original = input do { try self.p0.print(output, to: &input) } catch { input = original try self.p1.print(output, to: &input) } } }

6:44

This certainly seems reasonable, and it’s even quite similar to what we do in the parser, which also runs the p0 parser and only if it fails does it run the p1 parser: public func parse(_ input: inout P0.Input) rethrows -> P0.Output { let original = input do { return try self.p0.parse(&input) } catch let e0 { do { input = original return try self.p1.parse(&input) } catch let e1 { throw ParsingError.manyFailed( [e0, e1], at: input ) } } }

7:01

However, this is not the correct printer implementation, and this subtlety gets at the core of what it means for printers to reverse the effects of parsers.

7:11

To see the problem, let’s run the parser on two different names, one that needs quoting and one that does not: input = "" try field.print("Blob, Esq.", to: &input) input // ""Blob, Esq."" input = "" try field.print("Blob Jr.", to: &input) input // ""Blob Jr.""

7:30

Both names were quoted even though “Blob Jr.” doesn’t need to be quoted since it doesn’t contain a comma. Why is this happening?

7:41

Notice that the order of parsers we have listed in the OneOf builder closure matters quite a bit. If we flipped their order, so that we first try parsing until the comma, and if that fails when the try parsing the quotes: let _field = OneOf { Prefix { $0 != "," } quotedField }

7:54

We would have a parser that behaves substantially different. If we ran this parser on a string fragment that contained a quoted field with a comma, it would capture the opening quote and everything up until the comma: try _field.parse("\"Blob, Esq.\"") // "\"Blob"

8:17

The reason the order matters is because these parsers are not mutually exclusive. It’s possible for both to successfully parse the same string.

8:25

This is not something we have encountered with any of our OneOf usages that we’ve seen in previous episodes, such as the role parser or currency symbol parser. So far, all of our uses of OneOf have been to parse a string into the various cases of an enum with mutually exclusive parsers. Each parser can match one and only one case, and it wasn’t possible for two parsers to succeed on the same input.

8:49

Here we are in a situation where it’s possible for both parsers to succeed, and because of this we must order the parsers in a very particular manner. They must be ordered from most specific to least specific. Said another way, they must be ordered so that the first parser succeeds on the fewest number of inputs and the last succeeds on the most number of inputs. That is why we have put the parser that first consumes a quote first, because it is the more specific one compared to the one that simply consumes everything up until the next comma.

9:23

So far we’ve been explaining this subtlety from the perspective of parsing. We want to try running the most specific parser first and the least specific last. But printers are supposed to do the opposite of what parsers do. We should be trying to print the least specific one first and the most specific last. That would mean we first try printing our field in a non-quoted manner first, if possible, and only if that fails will we quote it.

9:47

So, we just got the order wrong in our OneOf2 printer. We need to first try printing with p1 and if that fails try p0 : extension Parsers.OneOf2: Printer where P0: Printer, P1: Printer { func print(_ output: P0.Output, to input: inout P0.Input) throws { let original = input do { try self.p1.print(output, to: &input) } catch { input = original try self.p0.print(output, to: &input) } } }

10:06

Amazingly, with that one change our printer now properly quotes the name only if it contains a comma: input = "" try field.print("Blob, Esq.", to: &input) input // ""Blob, Esq."" input = "" try field.print("Blob Jr.", to: &input) input // "Blob Jr."

10:18

This is our first true “wow” moment with printers, and we hope you feel the same.

10:22

We are expressing a moderately complex parser, but in a very familiar and natural manner: let field = OneOf { quotedField Prefix { $0 != "," } }

10:29

This simply says that to parse a field we are going to try one of two strategies: we can either capture the string between two quotes, or if there are no quotes we take everything up until a comma.

10:39

And then, almost magically, because all of these parsers have been turned into printers, and done so in a way that reverses the effects of the parsers, all of that nuanced quoting logic has been automatically carried over to the printer. If we try to print a string with a comma it is automatically quoted, and otherwise it is left unquoted.

10:59

Remember that previously we had to implement this printing logic in an ad hoc manner completely separate from the parser: func print(_ user: User, to input: inout String) { input.append(contentsOf: "\(user.id), ") if user.name.contains(",") { input.append(contentsOf: "\"\(user.name)\"") } else { input.append(contentsOf: user.name) } input.append(contentsOf: ", \(user.admin)") }

11:09

This is 7 lines of code involving string concatenation, string interpolation, and conditional statements. It’s honestly hard to read and quite a mess. Our parser-printer on the other hand packages everything up into a single unit. Skip parser-printing

11:22

This is already starting to look really amazing.

11:45

Now a lot of the constituents that make up the users parser have been turned into printers. Let’s conform the last remaining few.

12:08

We’ll start with the simplest ones. Take for example the Skip parser: Skip { "," zeroOrOneSpace }

12:38

Let’s see what it takes to make Skip into a printer. We can put in a stub conformance, preemptively constraining the wrapped Parsers generic to be a Printer since we know we’ll need that eventually: extension Skip: Printer where Parsers: Printer { func print(_ output: (), to input: inout Parsers.Input) throws { <#code#> } }

12:56

So we need to somehow print a Void value into a Parsers.Input . All we have at our disposal is self.parsers , which can print: extension Skip: Printer where Parsers: Printer { func print(_ output: (), to input: inout Parsers.Input) throws { self.parsers.print(<#Parsers.Output#>, to: &<#Parsers.Input#>) } }

13:09

But it only knows how to print Parsers.Output , which is a type we know nothing about.

13:18

One silly thing we could do to get things compiling is…well, nothing. A Skip skips the output being parsed, so maybe it should skip printing to the input? extension Skip: Printer where Parsers: Printer { func print(_ output: (), to input: inout Parsers.Input) throws { } }

13:30

But this is not what we want. If we take a look at how we use Skip , it’s holding onto a comma that we definitely want to be printed: Skip { "," zeroOrOneSpace }

13:46

But this is currently impossible to implement, and that is actually an OK thing. We should not expect that we can print a reasonable input when we have explicitly chosen to forget everything about the outputs via the Skip operator.

14:04

Consider a parser that consumes everything up to a comma and then skips to forget about the output. Skip { Prefix { $0 != "," } }

14:12

So if this were capable of being made into a printer, what we would we expect from printing a void value? input = "" Skip { Prefix { $0 != "," } } .print((), to: &input) input // ???

14:29

We have explicitly decided to forget all of the input we consumed, so we can’t possibly recover it in order to print it. This printer just is not possible.

14:44

However, there is a constraint we can put on Skip that makes it more reasonable. If the parser it wraps was forced to be a Void parser so that it had no meaningful output, then we could print: extension Skip: Printer where Parsers: Printer, Parsers.Output == Void { func print(_ output: (), to input: inout Parsers.Input) throws { try self.parsers.print((), to: &input) } }

15:15

And as soon as we do that this non-sensical printing example no longer compiles because Prefix is not a Void parser, and the compiler error message even tells us so: Skip { Prefix { $0 != "," } } .print((), to: &input) Referencing instance method ‘print(_:to:)’ on ‘Skip’ requires the types ‘Prefix<Substring>.Output’ (aka ‘Substring’) and ‘Void’ be equivalent

15:26

So the types are forcing us to prove that our parsers are Void before we are allowed to Skip them if we want to use printing functionality, and this is because we have no chance at printing unless the parser is a Void parser.

15:49

And it just so happens that all of our uses of Skip are precisely with Void parsers: Skip { "," zeroOrOneSpace } Both "," and zeroOrOneSpace are Void parsers, and so we are in good shape to make these printers.

16:04

The closure provided to Skip is a parser builder closure, which means the secretly it is using one of those Zip parsers under the hood in order to run each parser one after the other and discard the void values. In this case both parsers are void, and so the builder is using ZipVV , which represents a combination of two parsers that are both void.

16:29

We can make ZipVV a printer much like we did for

VOV 17:00

Now everything is compiling, and if we try printing a Skip parser we will see it just delegates all the printing to the parsers it wraps: input = "" Skip { "," zeroOrOneSpace } .print((), to: &input) input // ","

VOV 17:23

Now one interesting thing here is that it printed only "," and not ", " . Recall that previously when writing an ad-hoc parser for our user we decided to prefer an extra space after the commas. That is not happening here because zeroOrOneSpace is a OneOf : let zeroOrOneSpace = OneOf { " " "" }

VOV 17:41

And recall that the printers listed in a OneOf are tried in reverse order. So printing a zeroOrOneSpace will first try printing the empty string, which always succeeds, and so will then stop. This is why we don’t get a space after the comma. Later we will show how to customize the printing behavior of certain parsers so that we can prefer to have a space here. Int parser-printing

VOV 18:09

Just a few more parsers left. Let’s next look at the Int.parser() . If we command-click the .parser() static method we will find its definition: public static func parser( of inputType: Substring.Type = Substring.self, isSigned: Bool = true, radix: Int = 10 ) -> FromUTF8View< Substring, Parsers.IntParser<Substring.UTF8View, Self> > { .init { Parsers.IntParser<Substring.UTF8View, Self>( isSigned: isSigned, radix: radix ) } }

VOV 18:20

This is a lot to take in at once. The reason this looks more complicated than we might hope is that the parser that does the real work of parsing an integer actually works on a very low level representation of strings: collections of UTF-8 code units: extension Parsers { public struct IntParser< Input: Collection, Output: FixedWidthInteger >: Parser where Input.SubSequence == Input, Input.Element == UTF8.CodeUnit { … } }

VOV 18:41

This is because we want the integer parser to be as performant as possible, and if you are willing to work on the level of UTF-8 then you get a pretty big performance boost.

VOV 18:50

However, we know that not everyone wants to work on UTF-8, and many times just plain Substring is enough. So, we apply the FromUTF8View parser, which is a parser combinator that allows one to transform a parser on collections of UTF-8 code units into a parser of a different kind of input, such as substrings or unicode scalars: public struct FromUTF8View<Input, UTF8Parser: Parser> where UTF8Parser.Input == Substring.UTF8View { … }

VOV 19:12

All of that is to say, if we make both Parsers.IntParser and FromUTF8View both conform to the Printer protocol, then their composition will be printable, and thus Int.parser() will be printable.

VOV 19:31

To conform IntParser we need to turn an integer back into a collection of UTF8 code units. Now we can’t do this generically because we don’t know enough about Input . We only know it’s a collection of code units and that its SubSequence is Self which just means it comes with an efficient way to mutate itself: Input.SubSequence == Input, Input.Element == UTF8.CodeUnit

VOV 19:48

So, we need a little bit of extra information. There is a very general way to do this, but we aren’t yet ready for it, and so just like when we made Prefix into a printer, we are going to overly constrain this printer conformance to make things easier for now. Rather than being able to work on any collection of UTF-8 code units, we will just restrict to UTF8View s: extension Parsers.IntParser: Printer where Input == Substring.UTF8View { func print( _ output: Output, to input: inout Substring.UTF8View ) throws { … } }

VOV 20:47

But even with this overly constrained conformance it still isn’t entirely straightforward. While it is easy enough to transform the integer output into a UTF8View by first transforming it into a string: String(output).utf8

VOV 21:03

UTF8View s do not come with a way to append UTF-8 code units to them, so we can’t just append those bytes: func print(_ output: Output, to input: inout Substring.UTF8View) throws { input.append(contentsOf: String(output, radix: self.radix).utf8) } Value of type ‘Substring.UTF8View’ has no member ‘append’

VOV 21:16

This is because UTF8View s are more than just a plain collection of code units. They also do UTF-8 validation when constructing them so that you cannot construct a UTF8View consisting of invalid code units.

VOV 21:20

This is because it could potentially allow for constructing UTF8View s of invalid sequences of UTF-8 bytes, which should not be possible by virtue of the fact that UTF8View s are derived from strings that are guaranteed to always be valid by Swift. We are going to dive much deeper into this topic in the next episode, but for now suffice it to say that the standard library does not directly allow for this.

VOV 21:48

However, just because the operations they expose are supposed to transform valid UTF8View s into other valid UTF8View s does not mean it’s not possible to force it.

VOV 21:55

We can first move ourselves into the Substring world, do the appending there, and then travel back to the UTF8View world: extension Parsers.IntParser: Printer where Input == Substring.UTF8View { func print( _ output: Output, to input: inout Substring.UTF8View ) throws { var substring = Substring(input) substring.append(contentsOf: String(output, radix: self.radix)) input = substring.utf8 } }

VOV 22:16

This gets things compiling, and soon we will find ways to even drop the Input == Substring.UTF8View constraint so that it can work on even more types of collections of UTF8 code units.

VOV 22:27

It might look like we’re may be doing a lot of extra work here, like creating a new substring, but that this is not really the case, and Swift is just creating a new view into the existing string.

VOV 22:42

Also, this printer doesn’t even need to be throw ing, since it always succeeds. func print(_ output: Output, to input: inout Substring.UTF8View) /*throws*/ { … } FromUTF8View

VOV 22:51

Now let’s conform the FromUTF8View parser to the Printer protocol. As we said a moment ago, this parser’s job is to transform parsers that work on low-level UTF8View s into parsers that work on higher-level abstractions such as substrings and unicode scalars.

VOV 23:16

So, whenever we use Int.parser() to parse substrings like this: try Parse { "Hello " Int.parser() "!" } .parse("Hello 42!") // 42

VOV 23:37

Secretly this is actually being translated to this: try Parse { "Hello " FromUTF8View { Int.parser() } "!" } .parse("Hello 42!") // 42 Where Int.parser() works on the lower-level UTF-8 code units, and then FromUTF8View translates that parse into the world of simpler substrings.

VOV 23:56

It does this by being initialized with a UTF-8 parser and two transformations, one to turn a UTF8View into some Input and another to turn Input back into a UTF8View : public let utf8Parser: UTF8Parser public let toUTF8: (Input) -> Substring.UTF8View public let fromUTF8: (Substring.UTF8View) -> Input

VOV 24:12

Using all of this information we can create a parser of Input values by first converting the Input to a UTF8View , running the parser, and then converting the mutated UTF8View back to Input so that we can mutate the inout argument: public func parse( _ input: inout Input ) rethrows -> UTF8Parser.Output { var utf8 = self.toUTF8(input) defer { input = self.fromUTF8(utf8) } return self.utf8Parser.parse(&utf8) }

VOV 24:28

This is a fundamental operation that allows us to mix and match parsers that work on different string abstractions.

VOV 24:35

We can make this into a printer by copying the body of the parse method, and calling the parser’s print method instead: extension FromUTF8View: Printer where UTF8Parser: Printer { func print( _ output: UTF8Parser.Output, to input: inout Input ) throws { var utf8 = self.toUTF8(input) defer { input = self.fromUTF8(utf8) } try self.utf8Parser.print(output, to: &utf8) } }

VOV 25:34

Now the parser we sketched a moment ago is also a printer: input = "" try ParsePrint { "Hello " FromUTF8View { Int.parser() } "!" } .print(42, to: &input) input // "Hello 42!" Bool parser-printing

VOV 25:56

We are now very close to the user parser being a printer. The next parser we can tackle is this Bool.parser() . If we command-click that method we will be taken to its definition: public static func parser( of inputType: Substring.Type = Substring.self ) -> FromUTF8View< Substring, Parsers.BoolParser<Substring.UTF8View> > { .init { Parsers.BoolParser<Substring.UTF8View>() } }

VOV 26:11

This is very similar to Int.parser() . It’s actually composed of two parsers, a BoolParser that works on UTF8View s to be performant, and then that is transformed into a Substring parser via the FromUTF8View parser. We’ve already made FromUTF8View a printer, so all that’s left is to make BoolParser a printer, which is can be done very similarly to IntParser : extension Parsers.BoolParser: Printer where Input == Substring.UTF8View { func print(_ output: Bool, to input: inout Substring.UTF8View) { var substring = Substring(input) substring.append(contentsOf: String(output)) input = substring.utf8 } }

VOV 26:58

It’s a bummer we have to copy over the substring dance, but as we said before, we will be able to clean this up very soon. User parser-printing

VOV 27:39

We’re so close. Just one more parser left, which is the Zip parser that combines these five parsers together: let user = Parse(User.init(id:name:admin:)) { Int.parser() Skip { "," zeroOrOneSpace } field Skip { "," zeroOrOneSpace } Bool.parser() }

VOV 27:54

This is a ZipOVOVO because we are discarding the void values from the second and fourth parsers.

VOV 28:01

We need to make this type into a printer, which will be very similar to what we did for the ZipVOV and ZipVV parsers from before, so we’ll just paste in the final conformance: extension Parsers.ZipOVOVO: Printer where P0: Printer, P1: Printer, P2: Printer, P3: Printer, P4: Printer { func print( _ output: (P0.Output, P2.Output, P4.Output), to input: inout P0.Input ) throws { try self.p0.print(output.0, to: &input) try self.p1.print((), to: &input) try self.p2.print(output.1, to: &input) try self.p3.print((), to: &input) try self.p4.print(output.2, to: &input) } }

VOV 29:12

This simply uses each printer to print an output into the input.

VOV 29:18

We would now hope that we can finally use the user parser to print a user back into a comma-separated string: user.print(User(id: 42, name: "Blob", admin: true)) Referencing instance method ‘print’ on ‘Parse’ requires that ‘Parsers.Map<Parsers.ZipOVOVO<FromUTF8View<Substring, Parsers.IntParser<Substring.UTF8View, Int>>, Skip<Parsers.ZipVV<String, OneOf<Parsers.OneOf2<String, String>>>>, Parsers.Map<OneOf<Parsers.OneOf2<Parse<Parsers.ZipVOV<String, Prefix<Substring>, String>>, Prefix<Substring>>>, String>, Skip<Parsers.ZipVV<String, OneOf<Parsers.OneOf2<String, String>>>>, FromUTF8View<Substring, Parsers.BoolParser<Substring.UTF8View>>>, User>’ conform to ‘Printer’

VOV 29:28

However that does not work. It seems that the parser Parse is wrapping is a Parsers.Map with a bunch of other stuff inside. Remember that we haven’t yet made Parsers.Map into a printer due to some complications we are going to get into later, but why is this parser even showing up? We aren’t using .map anywhere.

VOV 29:42

Well, it turns out that when you use Parse with a transformation as we are doing here: let user = Parse(User.init(id:name:admin:)) { … }

VOV 29:50

That the initializer of Parse bakes in a .map using the User initializer. We can even jump to the initializer to see this explicitly: public init<Upstream, NewOutput>( _ transform: @escaping (Upstream.Output) -> NewOutput, @ParserBuilder with build: () -> Upstream ) where Parsers == Parsing.Parsers.Map<Upstream, NewOutput> { self.parsers = build().map(transform) }

VOV 30:00

So what we are seeing here is that because we haven’t yet made Parsers.Map into a printer we cannot use the initializer on Parse that uses a transformation. For now we will have to drop that transformation and just be content dealing with tuples: let user = Parse { … }

VOV 30:24

Now everything compiles, and we can print a tuple of user data into a comma-separated string: input = "" try user.print((42, "Blob", true), to: &input) input // "42,Blob,true"

VOV 30:57

Alright! We are now printing a full row of user data into a comma-separated value.

VOV 31:04

Even better, if we used a name with a comma then we automatically get a quoted field in the string: input = "" try user.print((42, "Blob, Esq.", true), to: &input) input // "42,\"Blob, Esq.\",true"

VOV 31:13

This is really amazing. The user parser is composed of a ton of parsers, but it gets a print method pretty much for free by virtue of fact that all the parsers it’s composed of are also printers! Many parser-printing

VOV 31:26

Now let’s turn to the last parser in our playground, the users parser, which makes use of the Many parser: let users = Many { user } separator: { "\n" } terminator: { End() }

VOV 31:32

We need to turn the Many parser into a printer, but before we can do that we have to remember all the ins and outs of the Many parser because the printer must faithfully undo all the work the parser does.

VOV 31:43

The Many parser works by running its element parser, in this case user , many times on the input, and between each invocation of the element parser we also run the separator parser. We stop parsing once the element parser or separator parser fails, and we do a little bit of extra work to make sure we don’t accidentally parse a trailing separator.

VOV 32:17

So this parser: input = "A,A,A,B" Many { "A" } separator: { "," }.parse("A,A,A,B") input // ",B" Will not consume the “,B” because the element parser fails on the “B” and so we back up the consumed input to what it was after the last successful element parser.

VOV 32:49

The Many parser does a lot more than just this. In fact, if we take a look at its definition we will see a much more complex parse method.

VOV 33:04

It not only handles minimum and maximums for the number of elements parsed, but it also allows accumulating the elements into a separate generic result type. We don’t use any of that stuff for our parser, so we are going to overly constrain our conformance to make it easier for us to implement for just our use case right now, but the real conformance in the library will take into account all of these other use cases.

VOV 33:24

Let’s get a stub of a conformance into place. We’ll extend Many to be a Printer when the Element is also a printer, and we’ll go ahead and constrain that the Result generic is just a plain array, which will make things a lot easier: extension Many: Printer where Element: Printer, Result == [Element.Output] { func print( _ output: [Element.Output], to input: inout Element.Input ) throws { <#code#> } }

VOV 34:01

So, how can we implement this print method? We need to somehow print an entire collection of outputs into a single input. We could start by iterating over the array of outputs and using the element printer to print each output into the input, which sounds exactly like the ad hoc work we were previously doing in the UsersPrinter , so let’s copy and paste it and make a few changes to generalize it. For one, instead of appending “,” directly, we can try to print using the Many parser’s separator: func print(_ output: [Element.Output], to input: inout Element.Input) throws { var firstElement = true for elementOutput in output { defer { firstElement = false } if !firstElement { try self.separator.print } … } }

VOV 34:50

In order to be able to print the separator we must constrain the Separator generic also conform to the Printer protocol: extension Many: Printer where Element: Printer, Result == [Element.Output], Separator: Printer { … }

VOV 34:57

Now we can invoke the .print method on the separator printer: try self.separator.print(<#Separator.Output#>, to: &<#Element.Input#>)

VOV 35:00

But now the question is what do we feed to this print method? It takes a Separator.Output as an argument, but Separator.Output is a completely unconstrained generic. We don’t know anything about it, and so we can’t possibly generate an output out of thin air to feed to print .

VOV 35:13

This is a similar situation we ran into when trying to make Skip a printer. It didn’t make any sense for the parser we were skipping to be just any parser of any kind of output. We had to further constrain it to be Void parser because that was the only way we could meaningfully print something.

VOV 35:28

So let’s do that so that you are only allowed to print a Many once you have proven that it’s separator outputs Void values: extension Many: Printer where Element: Printer, Result == [Element.Output], Separator: Printer, Separator.Output == Void { … }

VOV 35:37

And now we can print with the separator into the input: try self.separator.print((), to: &input)

VOV 35:48

And we can print the element using the element printer: try self.element.print(elementOutput, to: &input)

VOV 36:08

And now things are compiling! It’s pretty cool that it has basically the same shape as the ad hoc UsersPrinter we defined earlier, except it’s generic over any kind of element and separator parser. This means we’ve generically distilled the shape of printing many things a single time so that we can reuse it over and over.

VOV 36:24

Let’s take it for a spin! We should be able to feed an array of tuple user values to the users parser’s print method: input = "" try users.print([ (1, "Blob", true), (2, "Blob, Esq.", false) ], to: &input) input // "1,Blob,true\n2,"Blob, Esq.",false"

VOV 37:00

And it works! It printed both rows and even correctly quoted the name in the row it needed to quote. Next time: generalized printing

VOV 37:07

We think this is pretty incredible. We now have the basic infrastructure of printers in place. At its core it is just a protocol that exposes a single method for turning some type of output back into an input, and that process can fail. It is further expected that this print requirement plays nicely with the corresponding parse requirement, which consumes some input to turn it into an output, and that process can also fail.

VOV 37:30

This expectation is something we called “round-tripping”. If you parse an input into an output and print that output back into an input, then you should roughly get the same input as what you started with. Further, if you print an output to an input and parse that input to an output, then you should again roughly get the same output as what you started with. We say roughly because parsing and printing can be lossy operations, but the underlying content of the input or output should be unchanged.

VOV 37:58

With this infrastructure we’ve already converted a pretty impressive parser to also be a printer. We had a parser that could process a list of comma-separated fields into an array of tuples that represent a user’s id, name and admin status, and now it can print an array of tuples values back into a comma-separated string of users.

VOV 38:22

Further, there is some very nuanced logic embedded in this parser-printer, where we want to be able to parse quoted name fields so that we can allow for commas in the name, but at the same time we want to prefer printing without the quotes if the name doesn’t contain a comma. This logic can be tricky to get right, and previously we had it scattered in two different places: once for the parser and once for the printer. And now it’s all in just one place.

VOV 38:54

Now, we could keep moving forward by converting more and more parser combinators to be printer combinators, which would allow us to build up more and more complex printers, but there are some problems we need to address first. In order for us to get as far as we did with printers in the previous episodes we needed to make some simplifying assumptions that has greatly reduced the generality that our library aspires to have.

VOV 39:17

The first problem is that in a few places we had to needlessly constrain our printer conformances because their inputs were far too general for us to know how to print to them. For example, in the Prefix printer we just assumed that we were working on substrings rather than any collection, and for the integer and boolean parsers we assumed we were working on UTF8View s rather than any collection of UTF-8 code units. This unnecessarily limits the number of situations we can make use of these parsers.

VOV 39:46

The second problem we ran into was that we couldn’t figure out how to make the map operation into a printer. The types just didn’t line up correctly, so we abandoned it and decided not to use map anywhere in our parser-printers. This means that we aren’t actually parsing and printing User structs, but rather we are really dealing with just tuples of the data. We currently don’t have a printer-friendly way of bundling up the data in those tuples into a proper User struct. Losing map due to printing is a pretty big deal as it is a fundamental operation for massaging parser outputs into more well-structured data, so ideally we can recover this operation somehow.

VOV 40:34

Let’s tackle the first problem of us unnecessarily constraining the inputs of our parsers just to accommodate printing…next time! References Invertible syntax descriptions: Unifying parsing and pretty printing Tillmann Rendel and Klaus Ostermann • Sep 30, 2010 Note Parsers and pretty-printers for a language are often quite similar, yet both are typically implemented separately, leading to redundancy and potential inconsistency. We propose a new interface of syntactic descriptions, with which both parser and pretty-printer can be described as a single program using this interface. Whether a syntactic description is used as a parser or as a pretty-printer is determined by the implementation of the interface. Syntactic descriptions enable programmers to describe the connection between concrete and abstract syntax once and for all, and use these descriptions for parsing or pretty-printing as needed. We also discuss the generalization of our programming technique towards an algebra of partial isomorphisms. This publication (from 2010!) was the initial inspiration for our parser-printer explorations, and a much less polished version of the code was employed on the Point-Free web site on day one of our launch! https://www.informatik.uni-marburg.de/~rendel/unparse/ Unified Parsing and Printing with Prisms Fraser Tweedale • Apr 29, 2016 Note Parsers and pretty printers are commonly defined as separate values, however, the same essential information about how the structured data is represented in a stream must exist in both values. This is therefore a violation of the DRY principle – usually quite an obvious one (a cursory glance at any corresponding FromJSON and ToJSON instances suffices to support this fact). Various methods of unifying parsers and printers have been proposed, most notably Invertible Syntax Descriptions due to Rendel and Ostermann (several Haskell implementations of this approach exist). Another approach to the parsing-printing problem using a construct known as a “prism” (a construct Point-Free viewers and library users may better know as a “case path”). https://skillsmatter.com/skillscasts/16594-unified-parsing-and-printing-with-prisms Downloads Sample code 0180-parser-printers-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 .