Video #181: Invertible Parsing: Generalization
Episode: Video #181 Date: Mar 14, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep181-invertible-parsing-generalization

Description
Our parser-printer library is looking pretty impressive, but there are a couple problems we need to address. We have made some simplifying assumptions that have greatly reduced the generality our library aspires to have. We will address them by abstracting what it means for an input to be parseable and printable.
Video
Cloudflare Stream video ID: a8978ddb6fe7d9add1ddc6654b78c9a0 Local file: video_181_invertible-parsing-generalization.mp4 *(download with --video 181)*
References
- Discussions
- Invertible syntax descriptions: Unifying parsing and pretty printing
- Unified Parsing and Printing with Prisms
- 0181-parser-printers-pt4
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
And 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.
— 0:28
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.
— 0:56
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.
— 1:20
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.
— 1:52
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.
— 2:15
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 UTF8 code units. This unnecessarily limits the number of situations we can make use of these parsers.
— 2:44
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. Why generalize?
— 3:32
Let’s tackle the first problem of us unnecessarily constraining the inputs of our parsers just to accommodate printing.
— 3:43
Let’s take a look at why we want our printers to be as general as our parsers by taking a look at the marathon race parser that can process a moderately complex string holding information for the cities, entrance fees and geographic coordinate routes for a bunch of races. Let’s quickly run its benchmark so that we can see how fast it runs: name time std iterations ---------------------------------------------- Race.Parser 34208.000 ns ± 3.73 % 40189
— 4:22
So it takes about 34 microseconds to parse the input string. This is pretty fast, and we’ve done one important thing to try to make it quite fast. All the parsers in this file work on the UTF8View representation of strings, which is a collection of raw UTF-8 code units, which are just UInt8 bytes. Scanning and processing this collection is extremely fast because it is a random access collection, but it’s also more fraught because you have to worry about UTF-8 normalization, such as two sequences of differing bytes secretly represent the same visual character in a string.
— 5:37
In the case of our race parser we don’t have to worry about any of those complexities because the literal parsers we make use of tend to deal with simple ASCII or characters that only have one representation, such as the degree symbol.
— 5:53
However, sometimes things can be more complicated, and so if you don’t want to worry about those complexities you can choose to do your parsing on the Substring abstraction instead. Substring takes care of figuring out all of the complexities of UTF-8 normalization, but at the cost of degraded performance. Scanning and processing a Substring is a lot more expensive than a UTF8View because its elements are variable width and hence not random access. The reason for this is specifically because Substring is dealing with UTF-8 normalization.
— 6:27
So, let’s see this cost in real, tangible numbers. Let’s quickly convert this UTF8View parser into a Substring parser.
— 8:18
Before even running the benchmark it’s worth mentioning how easy it was to convert the UTF8View parser to a Substring parser. It goes to show that parsing on both string representations can basically look the same, and so you have all the tools necessary to decide which abstraction level you want to work on.
— 8:53
Running the benchmarks we see a huge slow down: name time std iterations ----------------------------------------------- Race.Parser 114916.000 ns ± 7.18 % 11239
— 8:58
Now it takes 114 microseconds to run, which means the UTF8View parser takes a quarter of the time to parse than the substring parser does. That’s a pretty significant cost for using Substring .
— 9:19
So for performance-sensitive applications of our parser library it can be a good idea to work on the UTF8View level. It’s even possible to mix and match abstraction levels in a single parser. For example, you may be able to parse the majority of your domain on the level of raw UTF-8 code units, but in a few key areas you want the assurances of parsing substrings.
— 9:43
You can do this by using the FromSubstring parser to temporarily leave the world of UTF8View , specify parsers on Substring , and then return to the UTF8View world. We don’t have any places where we need this in the race parser, but for a moment let’s assume that the degree character was not unique in UTF8. If it had multiple representations then you may feel more comfortable delegating that normalization work to Substring : private let latitude = Parse(*) { Double.parser() FromSubstring { "° " } northSouth } private let longitude = Parse(*) { Double.parser() FromSubstring { "° " } eastWest }
— 11:07
Now our parsing time has improved from around 114 microseconds to just 84 microseconds: name time std iterations ---------------------------------------------- Race.Parser 84250.000 ns ± 5.06 % 16239
— 11:18
Still not as good as working on UTF8View the whole time, but at least it’s something.
— 11:58
If we wanted to get really pedantic we could even just parse the degree symbol as a substring and then go back to the UTF8 world for the space: private let latitude = Parse(*) { Double.parser() FromSubstring { "°" } " ".utf8 northSouth } private let longitude = Parse(*) { Double.parser() FromSubstring { "°" } " ".utf8 eastWest } name time std iterations ---------------------------------------------- Race.Parser 79500.000 ns ± 4.34 % 17297
— 12:26
And now parsing times have improved a tiny bit more. UTF8View parsing is now only a third of the time instead of a quarter of the time.
— 12:44
So, performance tuning our parsers is really important, and we’d hate to lose that ability just because we want our parsers to also be printers. We need to figure out a way for printers to work on UTF8View s as well as Substring so that we can decide what string representation level we want to work on. RangeReplaceableCollection
— 13:00
Now that we are convinced that we need to generalize our printers to support all of these use cases, but the question is where do we start? Let’s look at some of the printers we came across that needed to unnecessarily constrain its input to something more concrete so that we could actually do some printing.
— 13:18
For example, when conforming the Prefix type to the Printer protocol we constrained it to only work on inputs of substrings: extension Prefix: Printer where Input == Substring { func print(_ output: Input, to input: inout Input) throws { guard output.allSatisfy(self.predicate!) else { throw PrintingError() } input.append(contentsOf: output) } }
— 13:40
We did this because Prefix ’s Input type parameter is very generic. It works on any kind of collection whose SubSequence is itself. Many types satisfy this constraint, such as substrings, their UTF8View s and UnicodeScalarView s, array slices, Data , and more, but the constraint alone is not enough for us to know how to append additional information to the end of an input.
— 14:07
So, just to get things moving we assumed the Input was a substring, because that is the problem we had at hand, and that allowed us to easily append to the end of an input substring: input.append(contentsOf: output)
— 14:16
However, this constraint means we can’t use this parser on the many different situations we would hope, such as UTF8View s for performance.
— 14:23
Similarly, when conforming IntParser to the Printer protocol we had to unnecessarily constraint the Input type to be Substring.UTF8View : 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 } }
— 14:52
We had to do this because, again, we needed some way of appending new information to the end of an input, but the Input generic for IntParser is too generic for us to be able to do that. All that is known about it is that it’s a collection of UTF8 code units, and such types do not support appending. This means this printer conformance won’t work if we wanted to parse on an even lower level string representation than UTF8Views , such as array slices of bytes. It will only work on UTF8View s.
— 15:20
And we saw this exact situation repeat itself with the BoolParser : extension Parsers.BoolParser: Printer where Input == Substring.UTF8View { func print(_ output: Bool, to input: inout Substring.UTF8View) throws { var substring = Substring(input) substring.append(contentsOf: String(output)) input = substring.utf8 } } Again the Input was forced to be a UTF8View in order to conform to the Printer protocol even though the Parser conformance works for any generic collection of UTF8 code units.
— 15:27
So, over and over we are seeing that we our parsers can work on super generic collections, but our printers need a little bit of extra structure: the ability to append a new collection of data to the end of a value. There is no abstraction in the Swift standard library that represents this, but there is something that is close, and that’s RangeReplaceableCollection .
— 16:01
According to the docs this is a protocol that represents: Note A collection that supports replacement of an arbitrary subrange of elements with the elements of another collection.
— 16:09
That doesn’t necessarily seem applicable to what we are dealing with here. We aren’t trying to replace arbitrary subranges of elements.
— 16:18
However, this replacement property is just a vast generalization of a far simpler feature that we actually do want to make use of. The RangeReplaceableCollection protocol also comes with an append(contentsOf:) method, which does exactly what we want: Note func append<S>(contentsOf: S) Adds the elements of a sequence or collection to the end of this collection. Required. Default implementation provided.
— 16:39
However, this method is implemented in terms of replacing subranges, because if we look at the source code we will see that append(contentsOf:) calls out to something called append : public mutating func append<S: Sequence>(contentsOf newElements: __owned S) where S.Element == Element { let approximateCapacity = self.count + newElements.underestimatedCount self.reserveCapacity(approximateCapacity) for element in newElements { append(element) } }
— 17:00
And append calls out to something called insert : public mutating func append(_ newElement: __owned Element) { insert(newElement, at: endIndex) }
— 17:07
And insert calls out to replaceSubrange : public mutating func insert( _ newElement: __owned Element, at i: Index ) { replaceSubrange(i..<i, with: CollectionOfOne(newElement)) }
— 17:29
So, the RangeReplaceableCollection does do what we want, it just so happens to also do a whole bunch of stuff that we don’t need, such as replacing ranges.
— 17:38
A whole bunch of types conform to RangeReplaceableCollection , including array slices, Data , UnicodeScalarView , Substring and more: Array ArraySlice AttributedString.CharacterView AttributedString.UnicodeScalarView ContiguousArray Data ApplicationMusicPlayer.Queue.Entries FilePath.ComponentView Slice String String.UnicodeScalarView Substring Substring.UnicodeScalarView
— 17:57
So, perhaps we can swap out the Substring constraint in our Prefix printer to instead only constrain on the input being a RangeReplaceableCollection : extension Prefix: Printer where Input: RangeReplaceableCollection { func print(_ output: Input, to input: inout Input) throws { guard output.allSatisfy(self.predicate!) else { throw PrintingError() } input.append(contentsOf: output) } }
— 18:06
This compiles because we are already using the append(contentsOf:) requirement that RangeReplaceableCollection exposes to us. This printer now works with many, many more input types, including that big list we just saw a moment of ago.
— 18:20
For example, we can now make a new version of the quoted field parser that works on UTF8View s: let _quotedField = ParsePrint { "\"".utf8 Prefix { $0 != .init(ascii: "\"") } "\"".utf8 }
— 18:47
As a parser this now works on the much more performant UTF8View level, which as we have seen many times on Point-Free comes with a huge performance boost over substrings. Generic struct ‘Parse’ requires that ‘Substring.UTF8View’ conform to ‘RangeReplaceableCollection’
— 18:55
However, it looks like UTF8View does not conform to RangeReplaceableCollection . An overgeneralized abstraction
— 19:03
So, RangeReplaceableCollection seems to support a lot of the use cases we are interested in, including multiple string representations, but it does not support UTF8View .
— 19:13
There is a very good reason that UTF8View s do not conform to RangeReplaceableCollection . It would allow you to construct sequences of UTF-8 code units that are not actually valid UTF-8 strings. All strings in Swift are guaranteed to be valid UTF-8 sequences.
— 19:37
It is impossible to construct a Swift string with invalid unicode. This shouldn’t be too surprising because Swift strings work on the level of characters, which are a single extended grapheme cluster. So when we see a string like a flag: "🇺🇸"
— 19:51
We only see one single character. There are no String APIs, including those that come from String ‘s conformance to RangeReplaceableCollection , that allow us to corrupt this string into invalid unicode.
— 20:08
If we drop down a level of abstraction to unicode scalars we will see that the single flag character is more complex, but still in some sense is incorruptible just like String . The UnicodeScalarView representing of the flag consists of two scalars: "🇺🇸".count // 1 "🇺🇸".unicodeScalars.count // 2 Array("🇺🇸".unicodeScalars) // [127482, 127480]
— 20:36
But each of these scalars corresponds to a perfectly valid string character, they are just smaller atomic units than the extended grapheme clusters that make up characters. We can even construct a scalar view with just one of the scalars and we still get a valid view: String.UnicodeScalarView([.init(127482)!]) // "🇺" String.UnicodeScalarView([.init(127480)!]) // "🇸"
— 21:15
The string represented by each of these scalars is some strange glyph corresponding to a single letter. When you put those letters together you get the flag for the country corresponding to that country code. In this case U+S corresponds to the US flag.
— 21:23
And it is for this reason that UnicodeScalarView also conforms to RangeReplaceableCollection . None of the APIs exposed to us from that protocol give us the tools necessary for breaking the validation of a unicode string.
— 21:35
Things change once we drop down one more level of abstraction to UTF8View s. Here we will see that the single flag character is even more complex than unicode scalars, where it is now represented by 8 code units: "🇺🇸".utf8.count // 8 Array("🇺🇸".utf8) // [240, 159, 135, 186, 240, 159, 135, 184]
— 21:59
Unlike unicode scalars and characters, each of these bytes does not represent a valid unicode string. In fact, the majority do not. They only become valid if they are put into a collection in the correct order.
— 22:29
For example, we can split the array in half and construct a string from each half: String(decoding: [240, 159, 135, 186], as: UTF8.self) // "🇺" String(decoding: [240, 159, 135, 184], as: UTF8.self) // "🇸"
— 22:41
And we recover the same strings that we constructed a moment ago using a single unicode scalar.
— 22:47
However, what if we didn’t split the array of 8 code units in the middle, but rather somewhere else: String(decoding: [240, 159], as: UTF8.self) // "�" String(decoding: [135, 186, 240, 159, 135, 184], as: UTF8.self) // "��🇸"
— 23:02
Each of these array of UTF8 code units are not valid unicode sequences. And when we try to turn them into strings we see that something weird has happened.
— 23:07
This strange “�” character is known as a “replacement character”, and it represents a character that is unknown or unrepresentable in unicode. So, Swift has decided that when trying to construct a string from an invalid sequence of UTF-8 code units it will replace the invalid bytes with this replacement character. It does this because of the requirement we mentioned a moment ago that every Swift string must be a valid unicode string. So, Swift makes something invalid into something valid by replacing the invalid stuff.
— 23:36
This is why UTF8View does not conform to RangeReplaceableCollection , and why UTF8View exposes a much smaller set of APIs for constructing and mutating values. If you did have access to these more powerful APIs for manipulating the bytes of a string directly you would have the ability to construct a UTF8View value that represents an invalid string.
— 23:58
For example, you could take the flag emoji and replace its first byte with some other value: var utf8 = "🇺🇸".utf8 utf8.replaceSubrange(0...0, with: [241]) Value of type ‘String.UTF8View’ has no member ’replaceSubrange’
— 24:22
Of course this doesn’t compile, but if it did it would mean that the UTF-8 view would be holding invalid bytes, and because a UTF8View is just a view into an underlying string, that means the underlying string would also be holding invalid bytes, which is exactly what Swift prevents us from doing. AppendableCollection
— 24:31
So, it seems our big idea to generalize to use RangeReplaceableCollection has already fallen short because one of our two main types of input we like to use doesn’t conform to the protocol. However, that’s OK. The RangeReplaceableCollection does a lot more than simply appending a collection to another collection. It can insert elements, it can created collections of repeated elements, it can reserve capacity for the collection, and more. We don’t need any of this power for printers.
— 24:57
So, for us it will be better to create a new abstraction that sits somewhere between Collection and RangeReplaceableCollection and adds the one additional requirement that we can append a collection to the end of a collection.
— 25:10
We will call this new abstraction AppendableCollection , for it represents collections that can append other collections to it, and we will copy and paste its one requirement from the RangeReplaceableCollection protocol: protocol AppendableCollection: Collection { mutating func append<S: Sequence>(contentsOf elements: S) where S.Element == Element }
— 25:38
It’s very easy for certain types to this protocol, such as substring, array slices, Data and even UnicodeScalarView : import Foundation extension Substring: AppendableCollection {} extension ArraySlice: AppendableCollection {} extension Data: AppendableCollection {} extension Substring.UnicodeScalarView: AppendableCollection {}
— 26:14
These all immediately conform because all of these types are RangeReplaceableCollection s and so they already have the append(contentsOf:) method implemented.
— 26:16
Let’s see what it takes to make UTF8View conform to this protocol: extension Substring.UTF8View: AppendableCollection { public mutating func append<S: Sequence>(contentsOf elements: S) where S.Element == Element { <#code#> } }
— 26:26
We need to somehow take a generic sequence of UTF-8 code units and append them to the end of self . What we can do is convert self and newElements to a Substring , which are RangeReplaceableCollection s of characters, then append them together, and then convert back to UTF8View : extension Substring.UTF8View: AppendableCollection { public mutating func append<S>(contentsOf newElements: S) where S: Sequence, String.UTF8View.Element == S.Element { var result = Substring(self) result.append(contentsOf: Substring(decoding: Array(newElements), as: UTF8.self)) self = result.utf8 } }
— 27:19
There are a few subtle things in this code to be aware of. We are constructing a Substring from an arbitrary sequence of UTF-8 code units in this line: Substring(decoding: Array(newElements), as: UTF8.self)
— 27:26
As we saw earlier, this initializer will validate the bytes it is given and replace invalid ones with the unicode replacement character. This means the actual bytes we append may differ from the ones passed into the function, but we think this is OK, since it matches the behavior of the initializer, and when it comes to printers. In fact, most UTF-8 views that we append during printing are views of fully validated strings, so appending an invalid slice should rarely, if ever, happen.
— 28:02
Another subtlety of this code is that it isn’t very efficient. We are creating strings, concatenating them, and then throwing them away just to get the underlying UTF8View .
— 28:18
But it is possible to make this more efficient. We can special case a hot path that we particularly want to optimize. For example, if the elements we are appending are already a UTF8View , which will be common, there’s no reason to turn it into an array just to decode it back into a Substring . There is already a very efficient way to turn UTF8View s into Substring s: public mutating func append<S: Sequence>(contentsOf newElements: S) where S.Element == Element { var str = Substring(self) switch newElements { case let newElements as Substring.UTF8View: str.append(contentsOf: Substring(newElements)) default: str.append(contentsOf: Substring(decoding: Array(newElements), as: UTF8.self)) } self = str.utf8 }
— 29:12
Now this allows for very efficient appending of UTF8View s, and the fully general case is still implemented, albeit less efficiently.
— 29:21
Now that we have the AppendableCollection abstraction we can start making use of it. For example, to make Prefix a printer we will now constrain the Input to be an AppendableCollection , not just a regular collection: extension Prefix: Printer where Input: AppendableCollection { func print(_ output: Input, to input: inout Input) throws { guard output.allSatisfy(self.predicate!) else { throw PrintingError() } input.append(contentsOf: output) } }
— 29:35
And this compiles without making any other changes.
— 29:36
The IntParser ’s Printer conformance can also be generalized to work with any AppendableCollection input, and now its implementation can simply call out to append(contentsOf:) : extension Parsers.IntParser: Printer where Input: AppendableCollection { func print(_ output: Output, to input: inout Input) { input.append(contentsOf: String(output).utf8) } }
— 30:09
Similarly for BoolParser : extension Parsers.BoolParser: Printer where Input: AppendableCollection { func print(_ output: Bool, to input: inout Input) { input.append(contentsOf: String(output).utf8) } }
— 30:33
And now our UTF-8 quoted field parser is close to compiling, but there’s a new parser we are making use of that needs to be made into a printer. We are now using a bare UTF8View to represent parsing and consuming that literal from the beginning of the input. Previously we were just using a string, which we had already made into a printer, and now we need to do the same for String.UTF8View : extension String.UTF8View: Printer { func print(_ output: (), to input: inout Substring.UTF8View) { input.append(contentsOf: self) } }
— 31:21
With this we can finally use fieldUTF8 as a printer, and has all of the nuanced logic baked inside it: var inputUtf8 = ""[...].utf8 try quotedFieldUtf8.print("Blob, Esq."[...].utf8, to: &inputUtf8) Substring(inputUtf8) // "\"Blob, Esq.\""
— 32:11
We can keep going with the UTF-8 parser-printer by making the full field parser that works on the level of UTF8View s: let fieldUtf8 = OneOf { quotedFieldUtf8 Prefix { $0 != .init(ascii: ",") } }
— 55:21
A zeroOrOneSpaceUtf8 parser: let zeroOrOneSpaceUtf8 = OneOf { " ".utf8 "".utf8 }
— 32:50
And a UTF-8 user parser: let userUtf8 = Parse { Int.parser() Skip { ",".utf8 zeroOrOneSpaceUtf8 } fieldUtf8 Skip { ",".utf8 zeroOrOneSpaceUtf8 } Bool.parser() }
— 33:15
And finally we can create a users parsers that works on the level of UTF8View s: let usersUtf8 = Many { userUtf8 } separator: { "\n".utf8 } terminator: { End() }
— 33:29
And we can give it a spin by printing an array of tuple data into a UTF8View newline separated rows of comma-separated fields: inputUtf8 = ""[...].utf8 try usersUtf8.print( [ (id: 1, name: "Blob"[...].utf8, true), (id: 2, name: "Blob, Esq."[...].utf8, false), ], to: &inputUtf8 ) Substring(inputUtf8) // "1,Blob,true\n2,Blob \"Esq.\",false" Next time: mapping parser-printers
— 34:15
So we are now able to parse and print on more general types of input, which means we do not need to sacrifice performance in order to unify parsing and printing.
— 34:29
This is looking incredible, but there’s still one glaring problem that we have no yet addressed. Earlier we came across some innocent looking code that simply mapped on a parser to transform its output and we found that we couldn’t make that operation printer-friendly. The types simply did not line up.
— 34:46
Well, we are finally ready to tackle this seemingly simple problem. Turns out it’s not quite so simple, and to solve this we really have to contort our minds in some uncomfortable ways and it truly gets at the heart of what it means for printing to be inverse of parsing.
— 35:02
So, let’s dig into the problem…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 0181-parser-printers-pt4 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 .