EP 178 · Invertible Parsing · Feb 21, 2022 ·Members

Video #178: Invertible Parsing: The Problem

smart_display

Loading stream…

Video #178: Invertible Parsing: The Problem

Episode: Video #178 Date: Feb 21, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep178-invertible-parsing-the-problem

Episode thumbnail

Description

We’ve spent many episodes discussing parsing, which turns nebulous blobs of data into well-structured data, but sometimes we need the “inverse” process to turn well-structured data back into nebulous data. This is called “printing” and can be useful for serialization, URL routing and more. This week we begin a journey to build a unified, composable framework for parsers and printers.

Video

Cloudflare Stream video ID: c84bb17d12bc6b6197fa7adff6663aa2 Local file: video_178_invertible-parsing-the-problem.mp4 *(download with --video 178)*

References

Transcript

0:05

We are extremely excited to start this next series of episodes. This topic is more than 4 years in the making. We first started experimenting with these concepts before even launching Point-Free and we’ve been developing, re-developing and refining them over and over and over ever since.

0:21

Our series of episodes on parsers is some of our most popular content, second only to our Composable Architecture episodes. People in the community have built some really amazing tools with the parsing library we open sourced over a year ago, including music notation parsers, math expression parsers, XML parsers, and even full-on programming language parsers.

0:42

Although we have gone really deep into parser topics, including composition, performance, low-level string processing, API ergonomics, error handling, and more, there’s still a huge topic that we haven’t touched upon at all. When building a parser we spend a lot of time getting to know all of the subtleties and edge cases of the input we are trying to parse. This can actually be quite a significant amount of work, and when it’s all done it is quite satisfying to see it turn nebulous blobs of input values into well-structured output values.

1:14

But, often you need to turn your well-structured data back into the nebulous blob of data. We call this inverse process “printing”.

1:24

For example, after much work you may come up with a way to parse a custom textual format into some well-structured Swift data type that is used in your application. But then, at a later time, you decide to add some editing capabilities to the application. The instant we do this we are confronted with the problem of how to turn values of your first-class Swift data type back into an unstructured string so that we can save that data back to disk or send it to a server.

1:50

Many of Apple’s parsers work this way too. Not only can you decode nebulous JSON data into a first-class type, but you can also encode values of the type back into JSON data. Further, not only can date and number formatters parse a string into a date or number, but they can also turn those values back into a formatted string. So even Apple’s APIs recognize that once you have accomplished a parsing problem there is a corresponding printing or formatting problem left to be solved.

2:18

So, the problem of trying to reverse the process of parsing is important enough that we should definitely discuss it. Turns out, we can develop the theory of printing in parallel to the theory of parsing. It allows us to tackle the problem of printing in small, bite-sized units and then piece together all the parts to form a large, complex printer.

2:36

That already sounds cool, but the most amazing part of this story is that we can build parsers and printers simultaneously. If you are careful enough, then the very act of describing your parser will simultaneously provide you with a printer such that if you parse and then print you get roughly the “same” thing you started with, and similarly if you print and then parse. The need for printers

3:00

Let’s start by motivating why we would even want unified parser-printers. Having a composable framework for parsing made sense because parsing is very difficult, but printing is typically quite a bit easier to accomplish.

3:14

Let’s take a look at a few of the sample parsers the library ships with just to get an understanding of a few printing domains. Take for example the marathon race parser, which is a moderately complex parser built to parse a textual description of a bunch of races, including their city, entrance fee and geographic coordinate route: let input = """ New York City, $300 40.60248° N, 74.06433° W 40.61807° N, 74.02966° W … --- Berlin, €100 13.36015° N, 52.51516° E 13.33999° N, 52.51381° E … --- London, £500 51.48205° N, 0.04283° E 51.47439° N, 0.0217° E … """

4:00

We parse this nebulous textual data into well-structured data represented by some Swift structs and enums: struct Race { let location: String let entranceFee: Money let path: [Coordinate] } struct Money { let currency: Currency let dollars: Int } enum Currency { case eur, gbp, usd } struct Coordinate { let latitude: Double let longitude: Double }

4:39

To go in the opposite direction of parsing we would need to find a way to print these data types back into a string. We aren’t going to do that now with the race parser, and instead focus on a simpler parser in a moment, but I think we can imagine in our minds how that would work and it probably wouldn’t be too hard. We’d just cook up little functions for turning each of these data types into a string by using copious amounts of string interpolation and concatenation. For example, a money parser would just turn the currency enum into the string it represents and then interpolate that into a string with the dollars integer.

5:16

So, it does indeed seem like writing printers is probably going to be a lot easier than parsers. But, just to be sure, let’s look at another example. In the Routing.swift file we have an example of how to build up a URLRequest parser, which allows you to process requests such as these: var postRequest = URLRequest(url: URL(string: "/episodes/1/comments")!) postRequest.httpMethod = "POST" postRequest.httpBody = Data( """ {"commenter": "Blob", "message": "Hi!"} """.utf8 ) let requests = [ URLRequest(url: URL(string: "/")!), URLRequest(url: URL(string: "/contact-us")!), URLRequest(url: URL(string: "/episodes")!), URLRequest(url: URL(string: "/episodes/1")!), URLRequest(url: URL(string: "/episodes/1/comments")!), URLRequest(url: URL(string: "/episodes/1/comments?count=20")!), postRequest, ]

5:54

…into first-class Swift data types that describe all the different places those requests can be routed to in our application: enum AppRoute: Equatable { case home case contactUs case episodes(EpisodesRoute) } enum EpisodesRoute: Equatable { case index case episode(id: Int, route: EpisodeRoute) } enum EpisodeRoute: Equatable { case show case comments(CommentsRoute) } enum CommentsRoute: Equatable { case post(Comment) case show(count: Int) } struct Comment: Decodable, Equatable { let commenter: String let message: String }

6:08

We’ve explored this kind of router a number of times on Point-Free, including on our navigation series , our modularity series , and most recently we showed how parser builders make constructing parsers like this a breeze.

6:21

But, as useful as it is to parse requests into enum values, it can be just as useful to print an enum value back into a request that would route back to the enum value. This is most important when building a website. Every single web framework, whether it’s Ruby on Rails, Node.js’s Express framework or Swift’s Vapor, comes with a way to route requests to particular portions of the application’s logic.

6:47

For example, Rails applications have a “routes.rb” file that describes all the URLs the site recognizes, and specifies what part of application logic to execute when a route is recognized: Rails.application.routes.draw do get "/users/:user_id/books/:book_id" => "books#fetch" end

7:03

This says that when a request comes into the server matching “/users/:user_id/books/:book_id” that it will be recognized and a fetch method on a books controller will be invoked. Further, whatever was matched in the :user_id and :book_id parameters will be bundled into a dictionary that will be accessible from the controller so that the application can perform special logic.

7:31

It is worth noting that the parameters extracted from the URL are not typed at all. You have to explicitly cast and defensively program against the types you expect them to be, and deal with type mismatches in your application’s logic.

7:45

This is similar to how express.js works, except you provide a callback closure directly in the route for when it is recognized: app.get('/users/:userId/books/:bookId', function (req, res) { … })

8:09

Again these parameters are not type safe, just like with Ruby. Now, we shouldn’t fault Rails and express.js too much for this because both Ruby and JavaScript are dynamic languages. They don’t prioritize a type system like Swift does.

8:22

Vapor, the web framework written in Swift, exposes a very similar API for routing requests, except you separate the path components in variadic arguments: app.get("users", ":userID", "books", ":bookID") { req in … }

8:48

This API has been closely modeled after express.js’s API, and sadly it also adopts a lack of type safety in the arguments. The :userID and :bookID parameters are bundled up into a [String: String] dictionary and then it is up to you to further transform it into the types you expect and handle when there is a type mismatch.

9:07

This is just a small sample of how a few popular web frameworks handle URL request routing. But routing is only half the story when it comes to building a server. We not only want to route incoming requests to specific webpages, but we also want to generate URLs that can be embedded in webpages for navigating our site.

9:26

For example, if we render a list of all the books associated with a user we would need to manually generate a whole bunch of HTML links like this: <a href="/users/42/book/123">Blob Autobiography</a> <a href="/users/42/book/321">Blobbed around the world</a> …

9:48

To generate those URLs we would need to literally interpolate a string like this: "/users/\(user.id)/book/\(book.id)"

10:07

This a little hard to read, but it may not bother you too much. However, the real trouble is that there is nothing guaranteeing that we keep the routing logic and the linking logic in sync. In fact, I actually have a typo here. The URL fragment should be “books” not “book”: "/users/\(user.id)/books/\(book.id)"

10:35

This typo would have meant that we were accidentally generating incorrect links on our site, which would result in 404s. That just shouldn’t be possible.

10:44

And these kinds of mistakes wouldn’t be possible if parsing and printing were unified in one package so that you didn’t have to repeat yourself for each task. The router should have the ability to turn the user id and book id into a request: router.print(.user(id: user.id, .book(id: book.id)))

11:23

This code would be 100% type safe. It would know that it needs two arguments, both of which are integers. You would not be allowed to pass anything else.

11:31

Interestingly, neither express.js nor Vapor try to solve for this problem, but Rails does and has for well over 10 years. They call it “named routes”, and every route you specify in a Rails application has a corresponding function that is magically generated for you: users_books_path 42, 321 # /users/42/books/321

12:04

(In Ruby, parentheses on function calls are optional.)

12:10

This is very cool, and honestly makes for a much better experience making a website when you know the framework is helping you correctly generate links within your site. However, it is not type safe at all. These functions are not statically known and so they cannot be autocompleted by an IDE. Instead, you must know how to write them out perfectly from memory, and there is nothing preventing you from passing nonsensical data to the function like a boolean instead of an integer. The function will happily take that data and try to do the best it can with it, or fail at runtime.

12:45

Our URL request parser accomplishes what these 3 web frameworks are trying to accomplish , but in a more concise and type safe manner. We get to deal with first-class Swift data types, rather than strings, and because it’s all built on parsers it is infinitely flexible and easy to add your own new parsers to the mix. And further, once we learn how to turn our router into a printer we will immediately get the ability to print 100% correct links to various parts of our site. Ad hoc printing

13:19

So now that we see that printing is definitely a useful concept, let’s start to explore what it takes to print some first-class Swift data types back into the nebulous description from which they can be parsed. We will write an ad hoc printer for some of the parsers we previously wrote. In our series of episodes on parser builders we eased into introducing result builder syntax by considering a parser that could process many rows of comma-separated values, which represents an array of users: import Parsing let usersCsv = """ 1,Blob,true 2,Blob Jr,false 3,Blob Sr,true """ struct User { var id: Int var name: String var admin: Bool } let user = Parse(User.init(id:name:admin:)) { Int.parser() "," Prefix { $0 != "," }.map(String.init) "," Bool.parser() } let users = Many { user } separator: { "\n" } terminator: { End() } var input = usersCsv[...] try users.parse(&input) input

15:11

This little bit of code packs a huge punch. Not only can we process any number of rows of this comma-separated text format, but we are transforming that nebulous textual data into well-structured, first-class data types, such as integers, booleans, and the User type.

15:29

Now suppose we want to go in the opposite direction. What if we have an array of users that we want to turn into a comma-separated list of values as a big string? And even better, what if we guaranteed that the printed string was valid so that our parser could successfully parse it too?

15:45

It’s pretty straightforward to do this in an ad hoc fashion. We can start by writing a function that is capable of printing a single user: func print(user: User) -> String { "\(user.id),\(user.name),\(user.admin)" }

16:09

And this works well enough: print(user: .init(id: 42, name: "Blob", admin: true)) // "42,Blob,true"

16:19

Then we can make a function that can print an entire collection of users, which will use the print(user:) function we just implemented under the hood: func print(users: [User]) -> String { users.map(print(user:)).joined(separator: "\n") } Though one bummer about this code is that we are needlessly creating an intermediate array, the users.map , just to immediately throw it away by joining it into a string. It would be more efficient to build up the string all at once, but that also takes more ad hoc work to do, so we’ll just leave it like this for now.

16:43

And this too works as we expect: let output = try users.parse(&input)! … print(users: output) // 1,Blob,true // 2,Blob Jr,false // 3,Blob Sr,true

17:00

Let’s verify that this parsing and printing logic can be round-tripped to arrive at where we started, if we parse and then print we get the same input, and if we print and then parse we get the same output: struct User: Equatable { … } input = usersCsv[...] try print(users: users.parse(&input)) == input // true try users.parse(print(users: output)) == output // true

17:54

And it is! So it seems that we have indeed written a proper printer for the comma-separated list of users.

18:08

This round-tripping property is very important. It is what gives us confidence that both parsing and printing are working as we expect. It assures us that when we print something we can successfully parse it again later.

18:20

Though we have accomplished what we set out to, there are some problems with this code. Although the printing code is quite short and simple enough, it exists completely outside the domain of the parsing, even though they are linked in a fundamental way. If we wanted to change some logic or fix a bug in the parser, we will have to remember to make the corresponding changes to the printer too. And vice versa, if we update or fix something in the printer we have to run over to the parser to make sure it is also up to date.

18:46

For example, right now our parser isn’t robust enough to handle user names that contain a comma: let usersCsv = """ 1,Blob,true 2,Blob Jr,false 3,Blob Sr,true 4,Blob, Esq.,true """

18:57

Parsing this will fail because the name parser consumes everything up until the second comma, and then it expects to have a boolean after that but instead it encounters the remainder of the name: “ Esq.”. We can even run the parser in order to check out the fancy new error messages that the library now emits as of our last release: error: multiple failures occurred error: unexpected input --> input:4:8 4 | 4,Blob, Esq.,true | ^ expected boolean error: unexpected input --> input:3:14 3 | 3,Blob Sr,true | ^ expected end of input

19:23

To remedy this we want to support the concept of parsing “quoted fields” from a row. That is, if a field between two commas is surrounded by quotes then we allow the field to contain as many commas as it wants. If our parser was aware of this special syntax then we could simply quote the field so that we are allowed to accept the comma in the name: var input = """ 1,Blob,true 2,Blob Jr,false 3,Blob Sr,true 4,"Blob, Esq.",true """[...]

19:43

Let’s see what it would take to support this new syntax in our parser.

19:46

Right now we parse the name by simply taking everything up until the next comma: Prefix { $0 != "," }.map(String.init)

19:52

Sounds like we now have two different parsers we want to try out on the name field, taking the one that succeeds. So, we want a OneOf parser: OneOf { }

20:08

First we will try parsing the quoted field, which we can do by consuming a quote, then everything up to the next quote for the name, and then the final quote: OneOf { Parse { "\"" Prefix { $0 != "\"" }.map(String.init) "\"" } }

20:29

And then if that fails we can fallback to simply taking everything up to the next comma: OneOf { Parse { "\"" Prefix { $0 != "\"" }.map(String.init) "\"" } Prefix { $0 != "," }.map(String.init) }

20:34

And since this parser is getting a little long, we may want to split it out into two parsers: let field = OneOf { Parse { "\"" Prefix { $0 != "\"" }.map(String.init) "\"" } Prefix { $0 != "," }.map(String.init) } let user = Parse(User.init(id:name:admin:)) { Int.parser() "," field "," Bool.parser() }

20:46

Further, rather than mapping on each of the Prefix parsers individually we can instead just map a single time on the OneOf parser: let field = OneOf { Parse { "\"" Prefix { $0 != "\"" } "\"" } Prefix { $0 != "," } } .map(String.init)

20:56

This is equivalent, and more generally any OneOf where we perform the same mapping operation on each parser on the inside is the same as moving all of those maps to a single one on the OneOf : OneOf { a.map(f) b.map(f) c.map(f) } == OneOf { a b c } .map(f)

21:24

If we run the playground we will see that we now successfully parse the string of users, obtaining an array of four users, including “Blob, Esq.”. However, our printing logic is now broken: 1,Blob,true 2,Blob Jr,false 3,Blob Sr,true 4,Blob, Esq.,true Printing does not surround “Blob, Esq.” in quotes, which means that our round-tripping logic that tries to re-parse this string fails.

21:53

The fix is easy enough. Let’s just always quote the name field when printing a user: func print(user: User) -> String { "\(user.id),\"\(user.name)\",\(user.admin)" }

21:59

And now parsing and printing do something reasonable: print(users: output) // "1,"Blob",true\n2,"Blob Jr",false\n3,"Blob Sr",true"

22:01

However, round-tripping is broken: print(users: users.parse(input)!) == input // false users.parse(print(users: output)) == output // true

22:05

Apparently what is printed from parsing the input does not exactly match the input.

22:12

This is because we are always quoting the name field no matter what, even if it does not contain a comma. Ideally we would only quote the field when necessary, which is when the name contains a comma. To capture that logic we have to muddy up our nice printing method: func print(user: User) -> String { "\(user.id),\(user.name.contains(",") ? "\"\(user.name)\"" : user.name),\(user.admin)" }

22:45

Now this prints the users with only “Blob, Esq.” quoted: 1,Blob,true 2,Blob Jr,false 3,Blob Sr,true 4,"Blob, Esq.",true And our round-tripping logic is back to working.

22:57

Further, what if we wanted to allow for at most one additional space after a comma, like this: 1, Blob, true 2, Blob Jr, false 3, Blob Sr, true 4, "Blob, Esq.", true

23:10

It should still parse if there’s no space after the comma, and the printing should prefer printing a space.

23:16

Well, to support this on the parser level we can introduce a small parser that consumes either zero or one spaces: let zeroOrOneSpace = OneOf { " " "" }

23:30

With this parser defined we can now update our user parser to allow for zero or one spaces after the commas: let user = Parse(User.init(id:name:admin:)) { Int.parser() "," zeroOrOneSpace field "," zeroOrOneSpace Bool.parser() } Extra argument in call

23:40

Unfortunately this will not work because we now have 7 parsers listed in this parser builder closure, and our library only ships with overloads for up to 6 parsers. We do this because adding more overloads for higher arity exponentially increases the code size of the library, and so we had to draw the line somewhere, and arity 6 seemed like a decent tradeoff.

24:05

It’s worth mentioning that this is just a temporary constraint on the library. There are certain Swift features on the horizon that will allow us to once and for all forget about how many parsers we are trying to run in a builder context.

24:17

Luckily there’s an easy workaround, and it’s actually quite similar to what one does in SwiftUI to workaround the fact that view builders are overloaded up to just arity 10. If you want to list more than 10 views inside a view builder context, like so: import SwiftUI VStack { Text("") Text("") Text("") Text("") Text("") Text("") Text("") Text("") Text("") Text("") Text("") } Extra argument in call

24:39

You can bundle up a few of these views into their own VStack or Group view in order to reduce the arity: VStack { Text("") Text("") Text("") Text("") Text("") Text("") Text("") Text("") Text("") Group { Text("") Text("") } }

24:51

We can do something similar for parsers. In this case we can group both the "," parser and the zeroOrOneSpace parser into a single Skip parser, which reduces the overall arity of this parser builder to 5: let user = Parse(User.init(id:name:admin:)) { Int.parser() Skip { "," zeroOrOneSpace } field Skip { "," zeroOrOneSpace } Bool.parser() }

25:14

If we run the playground we will see that the parsing works correctly, but the printing isn’t preferring to have a space after each comma: 1,Blob,true 2,Blob Jr,false 3,Blob Sr,true 4,"Blob, Esq.",true

25:35

To do that we need to update the print(user:) method yet again: func print(user: User) -> String { "\(user.id), \(user.name.contains(",") ? "\"\(user.name)\"" : user.name), \(user.admin)" }

25:43

And now it prints as we want: 1, Blob, true 2, Blob Jr, false 3, Blob Sr, true 4, "Blob, Esq.", true

25:50

So, it’s nice that everything is working as we expect, but also this is not code that we would want to maintain for the long term. It is a lot of responsibility for us to take on to make sure that every small change and fix to our parsers has to be simultaneously applied to the printer, and vice-versa.

26:06

And because the parsing and printing logic is scattered in two completely separate parts of the code base it’s hard to see the parallels between their logic. For example, supporting quoted fields in the parser is as simple as using a OneOf parser for describing the two formats of fields we allow: let field = OneOf { Parse { "\"" Prefix { $0 != "\"" } "\"" } Prefix { $0 != "," } } .map(String.init)

26:22

But the corresponding logic in the printing is a gross ternary expression of nested string interpolation: …\(user.name.contains(",") ? "\"\(user.name)\"" : user.name)...

26:34

These two pieces of logic look nothing alike. If we need to fix a bug in the logic we essentially have two very different problems before us. We must remember how the parser logic works so that we can fix, and then remember how the printing logic works so that we can fix it.

26:48

And if we fix something in the parser or printer without applying the corresponding fix in the other object, or if the logic was not faithfully reconstructed for both the parser and printer, then we can subtly break the entire thing.

27:00

For example, there is another UTF-8 character that looks a lot like a comma but is not: "," . What if I snuck that character into our printer: func print(user: User) -> String { "\(user.id), (user.name.contains(",") ? "\"\(user.name)\"" : user.name), \(user.admin)" }

27:13

This breaks round-tripping because we are now printing something that the parser does not recognize.

27:25

This should not be possible. In fact, all of these problems we are describing should not be possible. It should be that the very act of constructing a parser to transform a nebulous blob of data into something well-structured simultaneously constructs a printer that can transform that well-structured data back into the nebulous data. The whole forward and backward transformation should all be in one single package so that as soon as you fix a bug in the parser you are instantly fixing a bug in the printer, and vice-versa.

27:53

It should be possible to use the users value we constructed here to print an array of users into a comma-separated string: // print(users: output) users.print(output)

28:11

And this should be true of nearly all of our parsers. If we take a look at the benchmarks directory in the library we will see a bunch of parsers that accomplish a variety of tasks.

28:21

For example, that marathon race parser we looked at earlier. It processes a moderately complex text format: let input = """ New York City, $300 40.60248° N, 74.06433° W 40.61807° N, 74.02966° W … --- Berlin, €100 13.36015° N, 52.51516° E 13.33999° N, 52.51381° E … --- London, £500 51.48205° N, 0.04283° E 51.47439° N, 0.0217° E … """

28:29

And it accomplishes this with 11 different parsers that each concentrate on parsing a small piece of the above string, and then they are all pieced together to form one big parser. The very act of writing these parsers should simultaneously also describe how to print an array of races back into this string format.

28:45

Similarly for the URL router in the benchmarks. In just 40 lines of code it builds up a moderately complex parser that is capable of parsing an input URL request into one of 7 routes. The URL requests can be quite complicated, containing query parameters, JSON post bodies and more. It would be amazing if we could take this router and instantly be able to print a URL request from one of the AppRoute enum values. This has a ton of applications, both for iOS development and server-side development. Next time: a printer protocol

29:20

So, all of this leads us to want to find a better way. We shouldn’t have to spend a lot of time writing a parser, and then spend an equal amount of time writing a printer, and then always remember that we need to synchronize future updates of one to the other. Ideally we should be able to write parser and printer code at exactly the same time, in the same package, guaranteeing that they will stay in sync.

29:41

And amazingly, it is possible. And even better, the theory of parser-printers looks remarkably similar to just plain parsers, so everything we have learned so far will be applicable. There are only a few twists and turns along the way that we have to be mindful of.

29:56

Let’s first develop the theory of printers much like we did for parsers. We will distill its essence into a single function, and we will explore examples of printers as well as operators on printers that allow us to build large, complex printers from smaller ones. 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 0178-parser-printers-pt1 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 .