← All projects
In productionGleamStarted Apr 2025

Glindo

A parser-combinator library I built to learn functional programming and Gleam.

GleamParsingFunctional programmingLibrary

I started Glindo after watching Scott Wlaschin's talk on parser combinators on YouTube. I wanted to learn functional programming and Gleam, and building a library seemed like a good way to give those ideas somewhere to go. As for the name glindo, it's a combination of Gleam, the language I was learning to build parser combinators with, and Lindo, my partner's last name.

The idea is simple enough: build small parsers, then combine them into bigger ones. Working through that meant getting familiar with mapping, monadic sequencing, and lazy evaluation. Glindo now includes JSON and CSV parsers built from those same pieces, and it's published on Hex with examples on HexDocs.

Package details for glindo on Hex: a downloads chart for the last thirty days, 227 downloads of this version, 3 in the last seven days, and 343 all time
The package page on Hex, September 2026.

For a small example, here's the boolean parser from my JSON demo, with just the type it needs:

import gleam/io
import glindo/parsers as p
import glindo/types as t

pub type JsonValue {
  JsonBool(Bool)
}

pub fn json_bool() -> t.Parser(JsonValue) {
  p.chc_of([
    p.map(p.tok(p.prefix_str("true")), fn(_) { JsonBool(True) }),
    p.map(p.tok(p.prefix_str("false")), fn(_) { JsonBool(False) }),
  ])
}

pub fn main() {
  p.run(json_bool(), "true") |> io.debug()
}

It tries true, then false, and maps whichever matches into a JsonBool. The result is:

Ok(ParseResult(JsonBool(True), "", 4))

That's the value, the text left over (nothing here), and how far the parser got. The full demo combines these small parsers to handle arrays and objects too. I'm using the current glindo/parsers import here; the demo files still call it glindo/parser.

The number parser in my CSV demo is even smaller. With the same imports, this is the relevant piece:

pub type CSVal {
  CSVInt(Int)
}

fn csv_num() -> t.Parser(CSVal) {
  use num <- p.map(p.num())
  CSVInt(num)
}

Running p.run(csv_num(), "42,hello") gives:

Ok(ParseResult(CSVInt(42), ",hello", 2))

It takes the number and leaves the comma and next field for another parser. That's the bit I like about this approach. Each piece does a small job, and you build up from there.

And here's the talk that got me started:

Watch on YouTube.