{"article":{"slug":"parser-combinators","title":"Parser Combinators","subtitle":"Better than regex!","summary":"A hands-on TypeScript introduction to parser combinators: starting from a partial parser type, chaining parsers with sequencing and mapping, and building up a hand-rolled combinator library that parses real ping output into structured data, as a cleaner middle ground between regex and tree-sitter.","content_type":"tutorial","language":"en","canonical_url":"https://t3.at/blog/2026-10-17-parser-combinators/","author":{"name":"Florian","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"T3","url":"https://t3.at/","listing_slug":null,"listing":null},"topics":[{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Tutorials","slug":"tutorials","url":"https://listedarticles.com/topics/tutorials"},{"name":"JavaScript","slug":"javascript","url":"https://listedarticles.com/topics/javascript"},{"name":"Programming Languages","slug":"programming-languages","url":"https://listedarticles.com/topics/programming-languages"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1661,"reading_minutes":7,"published_at":"2026-10-17T00:00:00.000Z","added_at":"2026-10-18T11:13:05.274Z","updated_at":"2026-10-18T11:13:05.274Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/parser-combinators","markdown_url":"https://listedarticles.com/articles/parser-combinators.md","example":false,"citation":"Florian, T3. \"Parser Combinators.\" 17 Oct 2026. https://t3.at/blog/2026-10-17-parser-combinators/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://t3.at/blog/2026-10-17-parser-combinators/"},"body_markdown":"## Parser Combinators\n\n**Parsing** is the act of taking less structured input like text, and turning it into more structured data, like objects.\n\nMany of you will be familiar with using regex to parse text, and some of you might know about tree-sitter, a heavy-duty parsing library which powers the syntax highlighting in your code editor.\n\nBut regex gets messy on complex tasks, and tree-sitter often is overkill — for this middle ground, **parser combinators** are perfect. Not many people know about them, so allow me to show you!\n\n## The partial parser\n\nThe smallest unit of parsing we’ll be looking at is the **partial parser**. It gets to decide where to cut the input string, and then it returns a tuple consisting of *whatever* and the rest of the string.\n\nIt can be any old function, as long as it returns such a tuple! It can even use regex inside.\n\n▸Code definition·`pInteger`\n\n```\nconst pInteger: Parser<number> = (input: string) => {\n  const match = input.match(/^\\d+/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};\n```\n\n## Chaining\n\nA single parser function won’t do much on its own; but by applying multiple parsers in sequence, we can consume the entire input step by step.\n\n▸Code definition·`pHello`\n\n```\nconst pHello: Parser<string> = (input: string) => {\n  if (!input.startsWith(\"Hello\")) throw new Error();\n\n  return [\"Hello\", input.substring(5)];\n};\n```\n\n▸Code definition·`pIgnoreWhitespace`\n\n```\nconst pIgnoreWhitespace: Parser<null> = (input: string) => {\n  const match = input.match(/^\\s+/);\n  if (match == null) throw new Error();\n\n  const spaces = match[0];\n  return [null, input.substring(spaces.length)];\n};\n```\n\n▸Code definition·`pInteger`\n\n```\nconst pInteger: Parser<number> = (input: string) => {\n  const match = input.match(/^\\d+/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};\n```\n\n```\nconst [token1, input2] = pHello(\"Hello 100\");\nconst [token2, input3] = pIgnoreWhitespace(input2);\nconst [token3, input4] = pInteger(input3);\n\nconsole.log(token1, token2, token3); // \"Hello\", null, 100\nconsole.log(input4); // empty!\n```\n\n## Chaining\n\nOr, and this is where the real power of partial parsers comes in: we wrap the chain up as a new parser of its own.\n\n▸Code definition·`pHello100`\n\n```\nconst pHello100: Parser<[string, null, number]> = (input: string) => {\n  const [hello, rest1] = pHello(input);\n  const [space, rest2] = pIgnoreWhitespace(rest1);\n  const [number, rest3] = pInteger(rest2);\n\n  return [[hello, space, number], rest3];\n};\n```\n\n## pSeq\n\nOr we generalize even further and provide a function that does the chaining! “Feeding the rest of the string into the next parser” is super generic after all, and doesn’t really depend on what any single parser does.\n\nGetting the types right is tricky, but TypeScript is perfectly capable of expressing these complex variadic types.\n\n▸Code definition·`pSeq`\n\n```\ntype Parsers<T extends unknown[]> = { [K in keyof T]: Parser<T[K]> };\n\nconst pSeq = <T extends unknown[]>(...parsers: Parsers<T>): Parser<T> =>\n  (input) => {\n    const results = [] as unknown as T;\n    let rest = input;\n    for (const p of parsers) {\n      const [val, next] = (p as Parser<unknown>)(rest);\n      results.push(val);\n      rest = next;\n    }\n    return [results, rest];\n  };\n```\n\n## Combinators\n\nSince `pSeq` combines many parsers into a new parser, we call it a **parser combinator**.\n\nTake a moment to appreciate the functional madness going on: `pSeq` is a function that takes parsers, which are functions, and it returns another parser, which is also a function.\n\nAnd only then do we actually start the parsing, by calling the resulting function with data.\n\nIt’s like we are building up this construct of nested functions to create a complex program. Kinda cool!\n\n## By the way\n\nIf splitting a string into many tokens feels somehow related to `.reduce` then you have good instincts. What we are doing here is actually the dual, which is called an *unfold* in functional circles (`reduce` is referred to as a *fold*).\n\nWhile a fold uses intermediate state — the accumulator — to combine\na list of things into one, an unfold uses intermediate state to split one thing into many.\n\nYay, maths!\n\n## Tuples are unwieldy\n\nIf the point of parsing is to get structured, often human-readable data out of raw strings, then a tuple of values like `[\"Hello\", null, 100]` can hardly be the end goal.\n\nWe could of course map over the result to transform it into a more convenient shape.\n\n```\nconst pHello100 = pSeq(...);\nconst [result, rest] = pHello100(\"Hello 100\");\n\nconsole.log(result); // [\"Hello\", null, 100]\n\nconst parsed = {\n  greeting: result[0],\n  who: result[2],\n};\n```\n\n## pMap\n\nBut this is not in the spirit of parser combinators! Like before, we can also wrap the mapping into a new parser combinator itself.\n\n`pMap` takes a parser and a function; the function will be applied\nto the eventual result after parsing.\n\n▸Code definition·`pMap`\n\n```\nconst pMap = <T, R>(parser: Parser<T>, fn: (value: T) => R): Parser<R> =>\n  (input: string) => {\n    const [value, rest] = parser(input);\n    return [fn(value), rest];\n  };\n```\n\n```\nconst pGreeting = pMap(\n  pHello100,\n  ([greeting, _, who]) => ({ greeting, who }),\n);\n\nconst [parsed, rest] = pGreeting(\"Hello 100\");\n\nconsole.log(parsed); // { greeting: \"Hello\", who: 100 }\n```\n\n## pRepeat\n\nBefore we hop into a practical example, let me show the third fundamental parser combinator: `pRepeat` — it takes a given\nparser and applies it over and over again until it can’t.\n\nIt’s the `*` of regex, as a combinator!\n\n▸Code definition·`pRepeat`\n\n```\nconst pRepeat = <T>(parser: Parser<T>): Parser<T[]> =>\n  (input: string) => {\n    const values: T[] = [];\n    let rest = input;\n    while (true) {\n      try {\n        const [value, next] = parser(rest);\n        values.push(value);\n        rest = next;\n      } catch {\n        return [values, rest];\n      }\n    }\n  };\n```\n\n▸Code definition·`pNumber`\n\n```\nconst pNumber: Parser<number> = (input: string) => {\n  const match = input.match(/^\\d+/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};\n```\n\n▸Code definition·`pSemicolon`\n\n```\nconst pSemicolon: Parser<string> = (input: string) => {\n  if (!input.startsWith(\";\")) throw new Error();\n\n  return [\";\", input.substring(1)];\n};\n```\n\n```\nconst pNumberCsv = pRepeat(pSeq(pNumber, pSemicolon));\n\npNumberCsv(\"10;20;30;\");\n// [[[10, \";\"], [20, \";\"], [30, \";\"]], \"\"]\n```\n\n## Let’s build a parser!\n\nWe have enough tools now to start parsing actual, real-world data!\n\nAbove is a sample output of me pinging Cloudflare. There’s tons\nof useful info in there, so let’s parse it into structured data —\nhow about this:\n\n```\ntype PingResult = {\n  header: PingHeader;\n  packets: IcmpPacket[];\n  statistics: PingStatistics;\n};\n\ntype IcmpPacket = {\n  byteCount: number; from: string;\n  seq: number; ttl: number; time: number;\n};\n\ntype PingHeader = {\n  targetIp: string;        // 1.1.1.1\n  resolvedIp: string;      // 1.1.1.1 but in parentheses\n  icmpPayloadSize: number; // 56\n  totalPacketSize: number; // 84\n};\n\ntype PingStatistics = {\n  packetsTransmitted: number; packetsReceived: number;\n  packetLoss: number; time: number;\n  rtt: { min: number; avg: number; max: number; mdev: number };\n};\n```\n\n## Bits and Bobs\n\nWe have some recurring things in this output like decimal numbers or IP addresses. So let’s set those up first.\n\n▸Code definition·`pString`\n\n```\n// Exact text, like \"PING\"\nconst pString = (text: string): Parser<string> => (input: string) => {\n  if (!input.startsWith(text)) throw new Error();\n\n  return [text, input.substring(text.length)];\n};\n```\n\n▸Code definition·`pInteger`\n\n```\n// Whole numbers, like 64\nconst pInteger: Parser<number> = (input: string) => {\n  const match = input.match(/^\\d+/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};\n```\n\n▸Code definition·`pDecimal`\n\n```\n// Numbers with an optional fraction, like 68.7\nconst pDecimal: Parser<number> = (input: string) => {\n  const match = input.match(/^\\d+(\\.\\d+)?/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};\n```\n\n▸Code definition·`pIpV4Address`\n\n```\n// Four bytes and three dots, like 1.1.1.1\nconst pByte = pMap(pInteger, (n) => {\n  if (n > 255) throw new Error();\n  return n;\n});\n\nconst pDot = pString(\".\");\n\nconst pIpV4Address = pMap(\n  pSeq(pByte, pDot, pByte, pDot, pByte, pDot, pByte),\n  ([a, _1, b, _2, c, _3, d]) => `${a}.${b}.${c}.${d}`,\n);\n```\n\n▸Code definition·`pWhitespace`\n\n```\n// At least one space\nconst pWhitespace: Parser<null> = (input: string) => {\n  const match = input.match(/^\\s+/);\n  if (match == null) throw new Error();\n\n  return [null, input.substring(match[0].length)];\n};\n```\n\n## The header\n\nThe first line should be easy, it’s just some `pSeq` of parsers and then a `pMap` to finagle it into `PingHeader` shape.\n\n▸Code definition·`pPingHeaderParts`\n\n```\nconst pPingHeaderParts = pSeq(\n  pString(\"PING\"), pWhitespace, pIpV4Address, pWhitespace,\n  pString(\"(\"), pIpV4Address, pString(\")\"), pWhitespace,\n  pInteger, pString(\"(\"), pInteger, pString(\")\"),\n  pString(\" bytes of data.\"),\n);\n```\n\n▸Code definition·`pPingHeader`\n\n```\nconst pPingHeader = pMap(\n  pPingHeaderParts,\n  ([a, b, c, d, e, f, g, h, i, j, k, l, m]) => ({\n    targetIp: c,\n    resolvedIp: f,\n    icmpPayloadSize: i,\n    totalPacketSize: k,\n  }),\n);\n```\n\n## A packet\n\nActually the packets are also a `pSeq` of parsers, followed by a\n`pMap`. This parsing stuff is really easy, hey!\n\n▸Code definition·`pIcmpPacketParts`\n\n```\nconst pIcmpPacketParts = pSeq(\n  pInteger, pString(\" bytes from \"), pIpV4Address,\n  pString(\": icmp_seq=\"), pInteger,\n  pString(\" ttl=\"), pInteger,\n  pString(\" time=\"), pDecimal, pString(\" ms\"),\n);\n```\n\n▸Code definition·`pIcmpPacket`\n\n```\nconst pIcmpPacket = pMap(\n  pIcmpPacketParts,\n  ([a, b, c, d, e, f, g, h, i, j]) => ({\n    byteCount: a, from: c, seq: e, ttl: g, time: i,\n  }),\n);\n```\n\n## The statistics\n\nCould you imagine doing this in regex by the way?\n\n▸Code definition·`pUselessLine`\n\n```\nconst pUselessLine = pSeq(\n  pString(\"--- \"), pIpV4Address, pString(\" ping statistics ---\"),\n);\n```\n\n▸Code definition·`pStatsLine1`\n\n```\nconst pStatsLine1 = pSeq(\n  pInteger, pString(\" packets transmitted, \"),\n  pInteger, pString(\" received, \"),\n  pDecimal, pString(\"% packet loss, time \"),\n  pInteger, pString(\"ms\"),\n);\n```\n\n▸Code definition·`pStatsLine2`\n\n```\nconst pStatsLine2 = pSeq(\n  pString(\"rtt min/avg/max/mdev = \"),\n  pDecimal, pString(\"/\"), pDecimal, pString(\"/\"),\n  pDecimal, pString(\"/\"), pDecimal,\n  pString(\" ms\"),\n);\n```\n\n▸Code definition·`pPingStatistics`\n\n```\nconst pPingStatistics = pMap(\n  pSeq(pUselessLine, pWhitespace, pStatsLine1, pWhitespace, pStatsLine2),\n  ([_1, _2, [a, b, c, d, e, f, g, h], _3, [i, j, k, l, m, n, o, p, q]]) => ({\n    packetsTransmitted: a, packetsReceived: c,\n    packetLoss: e, time: g,\n    rtt: { min: j, avg: l, max: n, mdev: p },\n  }),\n);\n```\n\n## Ping packets, parsed!\n\nAnd boom, that’s pretty much it! Just one more `pSeq` and `pMap` to wrap all the parts together, and we have built a complete parser for the ping command.\n\nThat was super easy, right?\n\n▸Code definition·`pPingSummary`\n\n```\nconst pPingSummary: Parser<PingResult> = pMap(\n  pSeq(\n    pPingHeader,\n    pRepeat(pMap(pSeq(pWhitespace, pIcmpPacket), ([_, packet]) => packet)),\n    pWhitespace,\n    pPingStatistics,\n  ),\n  ([header, packets, _, statistics]) => ({ header, packets, statistics }),\n);\n```\n\n## Go give it a try\n\nIn a single post we built a fairly complicated parser from literally zero, and a hand-rolled parser combinator library along the way!\n\nIt’s much easier to create correct and maintainable parsers this way than with a regex; once you start nesting `pSeq` calls, the equivalent regex would become an unreadable mess, while the combinator approach gives you something squeaky clean.\n\nIn case you are missing fancier features like backtracking or lookahead, rest assured that parser combinators can handle those scenarios as well, since at the end of the day it’s all just functions composed together.\n\nGo give them a try next time you have to parse something — parser combinators are a really elegant solution to the parser problem, and many more people should use them!\n","body_html":"<h2 id=\"parser-combinators\">Parser Combinators</h2>\n<p><strong>Parsing</strong> is the act of taking less structured input like text, and turning it into more structured data, like objects.</p>\n<p>Many of you will be familiar with using regex to parse text, and some of you might know about tree-sitter, a heavy-duty parsing library which powers the syntax highlighting in your code editor.</p>\n<p>But regex gets messy on complex tasks, and tree-sitter often is overkill — for this middle ground, <strong>parser combinators</strong> are perfect. Not many people know about them, so allow me to show you!</p>\n<h2 id=\"the-partial-parser\">The partial parser</h2>\n<p>The smallest unit of parsing we’ll be looking at is the <strong>partial parser</strong>. It gets to decide where to cut the input string, and then it returns a tuple consisting of <em>whatever</em> and the rest of the string.</p>\n<p>It can be any old function, as long as it returns such a tuple! It can even use regex inside.</p>\n<p>▸Code definition·<code>pInteger</code></p>\n<pre><code>const pInteger: Parser&lt;number&gt; = (input: string) =&gt; {\n  const match = input.match(/^\\d+/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};</code></pre>\n<h2 id=\"chaining\">Chaining</h2>\n<p>A single parser function won’t do much on its own; but by applying multiple parsers in sequence, we can consume the entire input step by step.</p>\n<p>▸Code definition·<code>pHello</code></p>\n<pre><code>const pHello: Parser&lt;string&gt; = (input: string) =&gt; {\n  if (!input.startsWith(&quot;Hello&quot;)) throw new Error();\n\n  return [&quot;Hello&quot;, input.substring(5)];\n};</code></pre>\n<p>▸Code definition·<code>pIgnoreWhitespace</code></p>\n<pre><code>const pIgnoreWhitespace: Parser&lt;null&gt; = (input: string) =&gt; {\n  const match = input.match(/^\\s+/);\n  if (match == null) throw new Error();\n\n  const spaces = match[0];\n  return [null, input.substring(spaces.length)];\n};</code></pre>\n<p>▸Code definition·<code>pInteger</code></p>\n<pre><code>const pInteger: Parser&lt;number&gt; = (input: string) =&gt; {\n  const match = input.match(/^\\d+/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};</code></pre>\n<pre><code>const [token1, input2] = pHello(&quot;Hello 100&quot;);\nconst [token2, input3] = pIgnoreWhitespace(input2);\nconst [token3, input4] = pInteger(input3);\n\nconsole.log(token1, token2, token3); // &quot;Hello&quot;, null, 100\nconsole.log(input4); // empty!</code></pre>\n<h2 id=\"chaining-2\">Chaining</h2>\n<p>Or, and this is where the real power of partial parsers comes in: we wrap the chain up as a new parser of its own.</p>\n<p>▸Code definition·<code>pHello100</code></p>\n<pre><code>const pHello100: Parser&lt;[string, null, number]&gt; = (input: string) =&gt; {\n  const [hello, rest1] = pHello(input);\n  const [space, rest2] = pIgnoreWhitespace(rest1);\n  const [number, rest3] = pInteger(rest2);\n\n  return [[hello, space, number], rest3];\n};</code></pre>\n<h2 id=\"pseq\">pSeq</h2>\n<p>Or we generalize even further and provide a function that does the chaining! “Feeding the rest of the string into the next parser” is super generic after all, and doesn’t really depend on what any single parser does.</p>\n<p>Getting the types right is tricky, but TypeScript is perfectly capable of expressing these complex variadic types.</p>\n<p>▸Code definition·<code>pSeq</code></p>\n<pre><code>type Parsers&lt;T extends unknown[]&gt; = { [K in keyof T]: Parser&lt;T[K]&gt; };\n\nconst pSeq = &lt;T extends unknown[]&gt;(...parsers: Parsers&lt;T&gt;): Parser&lt;T&gt; =&gt;\n  (input) =&gt; {\n    const results = [] as unknown as T;\n    let rest = input;\n    for (const p of parsers) {\n      const [val, next] = (p as Parser&lt;unknown&gt;)(rest);\n      results.push(val);\n      rest = next;\n    }\n    return [results, rest];\n  };</code></pre>\n<h2 id=\"combinators\">Combinators</h2>\n<p>Since <code>pSeq</code> combines many parsers into a new parser, we call it a <strong>parser combinator</strong>.</p>\n<p>Take a moment to appreciate the functional madness going on: <code>pSeq</code> is a function that takes parsers, which are functions, and it returns another parser, which is also a function.</p>\n<p>And only then do we actually start the parsing, by calling the resulting function with data.</p>\n<p>It’s like we are building up this construct of nested functions to create a complex program. Kinda cool!</p>\n<h2 id=\"by-the-way\">By the way</h2>\n<p>If splitting a string into many tokens feels somehow related to <code>.reduce</code> then you have good instincts. What we are doing here is actually the dual, which is called an <em>unfold</em> in functional circles (<code>reduce</code> is referred to as a <em>fold</em>).</p>\n<p>While a fold uses intermediate state — the accumulator — to combine\na list of things into one, an unfold uses intermediate state to split one thing into many.</p>\n<p>Yay, maths!</p>\n<h2 id=\"tuples-are-unwieldy\">Tuples are unwieldy</h2>\n<p>If the point of parsing is to get structured, often human-readable data out of raw strings, then a tuple of values like <code>[&quot;Hello&quot;, null, 100]</code> can hardly be the end goal.</p>\n<p>We could of course map over the result to transform it into a more convenient shape.</p>\n<pre><code>const pHello100 = pSeq(...);\nconst [result, rest] = pHello100(&quot;Hello 100&quot;);\n\nconsole.log(result); // [&quot;Hello&quot;, null, 100]\n\nconst parsed = {\n  greeting: result[0],\n  who: result[2],\n};</code></pre>\n<h2 id=\"pmap\">pMap</h2>\n<p>But this is not in the spirit of parser combinators! Like before, we can also wrap the mapping into a new parser combinator itself.</p>\n<p><code>pMap</code> takes a parser and a function; the function will be applied\nto the eventual result after parsing.</p>\n<p>▸Code definition·<code>pMap</code></p>\n<pre><code>const pMap = &lt;T, R&gt;(parser: Parser&lt;T&gt;, fn: (value: T) =&gt; R): Parser&lt;R&gt; =&gt;\n  (input: string) =&gt; {\n    const [value, rest] = parser(input);\n    return [fn(value), rest];\n  };</code></pre>\n<pre><code>const pGreeting = pMap(\n  pHello100,\n  ([greeting, _, who]) =&gt; ({ greeting, who }),\n);\n\nconst [parsed, rest] = pGreeting(&quot;Hello 100&quot;);\n\nconsole.log(parsed); // { greeting: &quot;Hello&quot;, who: 100 }</code></pre>\n<h2 id=\"prepeat\">pRepeat</h2>\n<p>Before we hop into a practical example, let me show the third fundamental parser combinator: <code>pRepeat</code> — it takes a given\nparser and applies it over and over again until it can’t.</p>\n<p>It’s the <code>*</code> of regex, as a combinator!</p>\n<p>▸Code definition·<code>pRepeat</code></p>\n<pre><code>const pRepeat = &lt;T&gt;(parser: Parser&lt;T&gt;): Parser&lt;T[]&gt; =&gt;\n  (input: string) =&gt; {\n    const values: T[] = [];\n    let rest = input;\n    while (true) {\n      try {\n        const [value, next] = parser(rest);\n        values.push(value);\n        rest = next;\n      } catch {\n        return [values, rest];\n      }\n    }\n  };</code></pre>\n<p>▸Code definition·<code>pNumber</code></p>\n<pre><code>const pNumber: Parser&lt;number&gt; = (input: string) =&gt; {\n  const match = input.match(/^\\d+/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};</code></pre>\n<p>▸Code definition·<code>pSemicolon</code></p>\n<pre><code>const pSemicolon: Parser&lt;string&gt; = (input: string) =&gt; {\n  if (!input.startsWith(&quot;;&quot;)) throw new Error();\n\n  return [&quot;;&quot;, input.substring(1)];\n};</code></pre>\n<pre><code>const pNumberCsv = pRepeat(pSeq(pNumber, pSemicolon));\n\npNumberCsv(&quot;10;20;30;&quot;);\n// [[[10, &quot;;&quot;], [20, &quot;;&quot;], [30, &quot;;&quot;]], &quot;&quot;]</code></pre>\n<h2 id=\"let-s-build-a-parser\">Let’s build a parser!</h2>\n<p>We have enough tools now to start parsing actual, real-world data!</p>\n<p>Above is a sample output of me pinging Cloudflare. There’s tons\nof useful info in there, so let’s parse it into structured data —\nhow about this:</p>\n<pre><code>type PingResult = {\n  header: PingHeader;\n  packets: IcmpPacket[];\n  statistics: PingStatistics;\n};\n\ntype IcmpPacket = {\n  byteCount: number; from: string;\n  seq: number; ttl: number; time: number;\n};\n\ntype PingHeader = {\n  targetIp: string;        // 1.1.1.1\n  resolvedIp: string;      // 1.1.1.1 but in parentheses\n  icmpPayloadSize: number; // 56\n  totalPacketSize: number; // 84\n};\n\ntype PingStatistics = {\n  packetsTransmitted: number; packetsReceived: number;\n  packetLoss: number; time: number;\n  rtt: { min: number; avg: number; max: number; mdev: number };\n};</code></pre>\n<h2 id=\"bits-and-bobs\">Bits and Bobs</h2>\n<p>We have some recurring things in this output like decimal numbers or IP addresses. So let’s set those up first.</p>\n<p>▸Code definition·<code>pString</code></p>\n<pre><code>// Exact text, like &quot;PING&quot;\nconst pString = (text: string): Parser&lt;string&gt; =&gt; (input: string) =&gt; {\n  if (!input.startsWith(text)) throw new Error();\n\n  return [text, input.substring(text.length)];\n};</code></pre>\n<p>▸Code definition·<code>pInteger</code></p>\n<pre><code>// Whole numbers, like 64\nconst pInteger: Parser&lt;number&gt; = (input: string) =&gt; {\n  const match = input.match(/^\\d+/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};</code></pre>\n<p>▸Code definition·<code>pDecimal</code></p>\n<pre><code>// Numbers with an optional fraction, like 68.7\nconst pDecimal: Parser&lt;number&gt; = (input: string) =&gt; {\n  const match = input.match(/^\\d+(\\.\\d+)?/);\n  if (match == null) throw new Error();\n\n  const digits = match[0];\n  return [Number(digits), input.substring(digits.length)];\n};</code></pre>\n<p>▸Code definition·<code>pIpV4Address</code></p>\n<pre><code>// Four bytes and three dots, like 1.1.1.1\nconst pByte = pMap(pInteger, (n) =&gt; {\n  if (n &gt; 255) throw new Error();\n  return n;\n});\n\nconst pDot = pString(&quot;.&quot;);\n\nconst pIpV4Address = pMap(\n  pSeq(pByte, pDot, pByte, pDot, pByte, pDot, pByte),\n  ([a, _1, b, _2, c, _3, d]) =&gt; `${a}.${b}.${c}.${d}`,\n);</code></pre>\n<p>▸Code definition·<code>pWhitespace</code></p>\n<pre><code>// At least one space\nconst pWhitespace: Parser&lt;null&gt; = (input: string) =&gt; {\n  const match = input.match(/^\\s+/);\n  if (match == null) throw new Error();\n\n  return [null, input.substring(match[0].length)];\n};</code></pre>\n<h2 id=\"the-header\">The header</h2>\n<p>The first line should be easy, it’s just some <code>pSeq</code> of parsers and then a <code>pMap</code> to finagle it into <code>PingHeader</code> shape.</p>\n<p>▸Code definition·<code>pPingHeaderParts</code></p>\n<pre><code>const pPingHeaderParts = pSeq(\n  pString(&quot;PING&quot;), pWhitespace, pIpV4Address, pWhitespace,\n  pString(&quot;(&quot;), pIpV4Address, pString(&quot;)&quot;), pWhitespace,\n  pInteger, pString(&quot;(&quot;), pInteger, pString(&quot;)&quot;),\n  pString(&quot; bytes of data.&quot;),\n);</code></pre>\n<p>▸Code definition·<code>pPingHeader</code></p>\n<pre><code>const pPingHeader = pMap(\n  pPingHeaderParts,\n  ([a, b, c, d, e, f, g, h, i, j, k, l, m]) =&gt; ({\n    targetIp: c,\n    resolvedIp: f,\n    icmpPayloadSize: i,\n    totalPacketSize: k,\n  }),\n);</code></pre>\n<h2 id=\"a-packet\">A packet</h2>\n<p>Actually the packets are also a <code>pSeq</code> of parsers, followed by a\n<code>pMap</code>. This parsing stuff is really easy, hey!</p>\n<p>▸Code definition·<code>pIcmpPacketParts</code></p>\n<pre><code>const pIcmpPacketParts = pSeq(\n  pInteger, pString(&quot; bytes from &quot;), pIpV4Address,\n  pString(&quot;: icmp_seq=&quot;), pInteger,\n  pString(&quot; ttl=&quot;), pInteger,\n  pString(&quot; time=&quot;), pDecimal, pString(&quot; ms&quot;),\n);</code></pre>\n<p>▸Code definition·<code>pIcmpPacket</code></p>\n<pre><code>const pIcmpPacket = pMap(\n  pIcmpPacketParts,\n  ([a, b, c, d, e, f, g, h, i, j]) =&gt; ({\n    byteCount: a, from: c, seq: e, ttl: g, time: i,\n  }),\n);</code></pre>\n<h2 id=\"the-statistics\">The statistics</h2>\n<p>Could you imagine doing this in regex by the way?</p>\n<p>▸Code definition·<code>pUselessLine</code></p>\n<pre><code>const pUselessLine = pSeq(\n  pString(&quot;--- &quot;), pIpV4Address, pString(&quot; ping statistics ---&quot;),\n);</code></pre>\n<p>▸Code definition·<code>pStatsLine1</code></p>\n<pre><code>const pStatsLine1 = pSeq(\n  pInteger, pString(&quot; packets transmitted, &quot;),\n  pInteger, pString(&quot; received, &quot;),\n  pDecimal, pString(&quot;% packet loss, time &quot;),\n  pInteger, pString(&quot;ms&quot;),\n);</code></pre>\n<p>▸Code definition·<code>pStatsLine2</code></p>\n<pre><code>const pStatsLine2 = pSeq(\n  pString(&quot;rtt min/avg/max/mdev = &quot;),\n  pDecimal, pString(&quot;/&quot;), pDecimal, pString(&quot;/&quot;),\n  pDecimal, pString(&quot;/&quot;), pDecimal,\n  pString(&quot; ms&quot;),\n);</code></pre>\n<p>▸Code definition·<code>pPingStatistics</code></p>\n<pre><code>const pPingStatistics = pMap(\n  pSeq(pUselessLine, pWhitespace, pStatsLine1, pWhitespace, pStatsLine2),\n  ([_1, _2, [a, b, c, d, e, f, g, h], _3, [i, j, k, l, m, n, o, p, q]]) =&gt; ({\n    packetsTransmitted: a, packetsReceived: c,\n    packetLoss: e, time: g,\n    rtt: { min: j, avg: l, max: n, mdev: p },\n  }),\n);</code></pre>\n<h2 id=\"ping-packets-parsed\">Ping packets, parsed!</h2>\n<p>And boom, that’s pretty much it! Just one more <code>pSeq</code> and <code>pMap</code> to wrap all the parts together, and we have built a complete parser for the ping command.</p>\n<p>That was super easy, right?</p>\n<p>▸Code definition·<code>pPingSummary</code></p>\n<pre><code>const pPingSummary: Parser&lt;PingResult&gt; = pMap(\n  pSeq(\n    pPingHeader,\n    pRepeat(pMap(pSeq(pWhitespace, pIcmpPacket), ([_, packet]) =&gt; packet)),\n    pWhitespace,\n    pPingStatistics,\n  ),\n  ([header, packets, _, statistics]) =&gt; ({ header, packets, statistics }),\n);</code></pre>\n<h2 id=\"go-give-it-a-try\">Go give it a try</h2>\n<p>In a single post we built a fairly complicated parser from literally zero, and a hand-rolled parser combinator library along the way!</p>\n<p>It’s much easier to create correct and maintainable parsers this way than with a regex; once you start nesting <code>pSeq</code> calls, the equivalent regex would become an unreadable mess, while the combinator approach gives you something squeaky clean.</p>\n<p>In case you are missing fancier features like backtracking or lookahead, rest assured that parser combinators can handle those scenarios as well, since at the end of the day it’s all just functions composed together.</p>\n<p>Go give them a try next time you have to parse something — parser combinators are a really elegant solution to the parser problem, and many more people should use them!</p>","headings":[{"level":2,"text":"Parser Combinators","id":"parser-combinators"},{"level":2,"text":"The partial parser","id":"the-partial-parser"},{"level":2,"text":"Chaining","id":"chaining"},{"level":2,"text":"Chaining","id":"chaining-2"},{"level":2,"text":"pSeq","id":"pseq"},{"level":2,"text":"Combinators","id":"combinators"},{"level":2,"text":"By the way","id":"by-the-way"},{"level":2,"text":"Tuples are unwieldy","id":"tuples-are-unwieldy"},{"level":2,"text":"pMap","id":"pmap"},{"level":2,"text":"pRepeat","id":"prepeat"},{"level":2,"text":"Let’s build a parser!","id":"let-s-build-a-parser"},{"level":2,"text":"Bits and Bobs","id":"bits-and-bobs"},{"level":2,"text":"The header","id":"the-header"},{"level":2,"text":"A packet","id":"a-packet"},{"level":2,"text":"The statistics","id":"the-statistics"},{"level":2,"text":"Ping packets, parsed!","id":"ping-packets-parsed"},{"level":2,"text":"Go give it a try","id":"go-give-it-a-try"}]}}