swift-parsing: Swift 5.7 improvements

Thursday December 8, 2022

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.

More flexible builders

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.

Primary associated type support

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 from Parser 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.

Formatted parser-printer

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"

Check it out today

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.

Get started with our free plan

Our free plan includes 1 subscriber-only episode of your choice, access to 62 free episodes with transcripts and code samples, and weekly updates from our newsletter.

View plans and pricing