Parsing is the process of transforming unstructured input, such as raw text, into structured data like objects or lists. While regular expressions (regex) are common for simple tasks, they often become unmanageable for complex structures. Conversely, heavy-duty libraries like tree-sitter can be overkill for many applications. Parser combinators provide a functional alternative, allowing developers to build complex parsing logic by combining small, specialized functions.

This image illustrates the point where regular expressions become unmanageable for complex structures like HTML.
At the core of this approach is the 'partial parser.' A partial parser is a function that takes a string as input and returns a tuple containing two elements: the piece of data it successfully extracted and the remainder of the string. For example, a parser designed to identify an integer will scan the start of a string, extract the digits, and pass the rest of the string to the next function in the chain. By 'chaining' these functions—where the output of one becomes the input for the next—developers can consume an entire string step by step. This process is conceptually similar to an 'unfold' in functional programming, which uses intermediate state to split one thing into many, as opposed to a 'fold' (or reduce) which combines many things into one.
The true power of this method lies in the 'combinator'—a function that takes parsers as arguments and returns a new, more complex parser. For instance, a sequence combinator can take three individual parsers and wrap them into a single function that executes them in order. This allows for high-level abstractions, such as a parser that specifically looks for a greeting followed by a number. Furthermore, mapping combinators can be used to transform the raw extracted data into specific objects (like a JSON-style dictionary) during the parsing process itself. While this approach can sometimes lead to more 'unwrapping' code compared to parser generators that use separate grammar files, it offers a highly modular and readable way to build complex logic from simple, testable units.
One practical limitation of this manual approach is the complexity of managing types, especially in languages like TypeScript, where variadic types for sequences can be difficult to express. Additionally, while combinators are highly flexible, they require the developer to manually define the sequence of operations, which can be less efficient than a generator that automatically derives the parsing logic from a formal grammar. However, for many middle-ground tasks, the ability to compose parsers like building blocks provides a clear and maintainable path for handling structured data.