It’s the season of Advent of Code, and over the years, many reach for our Parsing library to solve its puzzles. So we wanted to take the time to make their experience a bit nicer this year by bringing in several quality-of-life improvements to Swift 5.7 users.
Our 0.11.0 release removes many limitations placed on parser builders in the previous release of the library, as well as introduces primary associated types, and the ability to use formatters (e.g., date formatters, number formatters, and more) in your parsers and parser-printers.
The library’s builders have limited the number of parsers allowed in a block because of a great number of overloads that need to be maintained and code generated, causing a bloat in binary size and compilation times.
For example, @OneOfBuilder
, which tries each parser given to the block till one succeeds, was previously limited to 10 parsers, which is similar to the limit SwiftUI imposes on ViewBuilder
blocks. This means if you were trying to parse an input string into an enum with more than 10 cases, you would need to nest the OneOf
parsers to get around this limitation:
let router = OneOf {
OneOf {
…
}
OneOf {
…
}
OneOf {
…
}
}
Meanwhile, @ParserBuilder
, which breaks parsing jobs down into small incremental steps for each parser passed to the block, was previously limited to only 6 parsers due to an exponential number of buildBlock
overloads that had to be code generated: hundreds of overloads were required to support 6 parsers in a block, and thousands would be required to support 7 or more!
This limitation broke down quickly. For example, to parse a parentheses-surrounded and comma-separated set of values into a User
type, you should be able to do this:
let user = Parse(User.init) {
"("
Int.parser()
","
Prefix { $0 != "," }
","
Bool.parser()
")"
}
But, that parser fails to compile because it is combining 7 parsers. To work around this limitation you would need to nest the Parse
builder contexts:
let user = Parse {
"("
Parse(User.init) {
Int.parser()
","
Prefix { $0 != "," }
","
Bool.parser()
}
")"
}
Thankfully, Swift 5.7 comes with a brand new result builder feature called buildPartialBlock
, which allows us to eliminate many of these restrictions and overloads, and improve library ergonomics, compile times, and even binary size!
@OneOfBuilder
no longer has a limit at all: you can simply list as many parsers as needed (or as many as the Swift compiler can handle).
And @ParserBuilder
now supports any number of Void
parsers and up to 10 non-Void
parsers. While it is still limited, it is not nearly as bad as it used to be since the majority of parsers in a builder context tend to be Void
-parsers. For example, in the user
parser above, 4 of the 7 parsers are Void
.
Parsing is powered by a number of protocols with associated types, including:
The
Parser
protocol, which is the fundamental unit of the library, and describes transforming a blob of nebulous data into something more structured.The
ParserPrinter
protocol, which inherits fromParser
but comes with a superpower: it can “print” structured data back into the nebulous blob from whence it came.The
Conversion
protocol, which parser-printers leverage for transforming parsed data in an invertible way.The
PrependableCollection
protocol, which parser-printers use to reverse the process of parsing. This is a strange protocol that even took us a long time to grapple with its mind-bending nature.
All four of these protocols have associated types that should take advantage of Swift 5.7’s new primary associated types:
Parser<Input, Output>
ParserPrinter<Input, Output>
Conversion<Input, Output>
PrependableCollection<Element>
This change allows you to express and constrain these protocols in a more lightweight, natural manner, especially with the use of opaque some
types.
We’ve also introduced a brand-new Formatted
parser-printer, which is compatible with Apple’s entire family of formatters, including byte formatters, date formatters, number formatters, and many more!
Simply pass the formatter to Formatted
to take advantage of many of the complex formats Apple provides for us.
let total = ParsePrint {
"TOTAL: "
Formatted(.currency(code: "USD"))
}
try total.parse("TOTAL: $42.42") // 42.42
try total.print(99.95) // "TOTAL: $99.95"
This is only scratching the surface. There is a lot more offered in the library. Check out our free video tour for more information, and give the library a spin to explore its new capabilities.