Current section
Files
Jump to
Current section
Files
src/pears.gleam
//// ## A parser combinator library for gleam
////
//// Writing a parser using pears is easy, you just combine functions that
//// parse smaller parts of the input into a function that parses the whole input.
////
//// Here's an example of a parser that parses the letter 'a':
////
//// ```gleam
//// let a_parser = just("a")
//// ```
////
//// We can run this parser using the `parse_string` function:
////
//// ```gleam
//// parse_string("abc", a_parser)
//// // => Ok(Parsed(["b", "c"], "a"))
//// ```
////
//// It returns a `Result` that gives us the remaining input (if any) and the parsed value when successful.
//// Here the remaining input is `["b", "c"]` and the parsed value is `"a"`.
////
//// Under the hood all `parse_string` does is convert the input string into a list of graphemes and then
//// call given parser, which is just a function, passing the input to it.
////
//// It is equivalent to the following:
////
//// ```gleam
//// let input = string.to_graphemes("abc")
//// a_parser()(input)
//// // => Ok(Parsed(["b", "c"], "a"))
//// ```
////
//// ### Combinators
////
//// Combining parsers using combinators is how you build more complex parsers.
//// A combinator is just a function that one or more parsers and returns a new parser.
////
//// Let's say we want to parse the letter 'a' followed by the letter 'b', we can use the `pair` combinator for that:
////
//// ```gleam
//// let ab_parser = pair(just("a"), just("b"))
//// parse_string("abc", ab_parser)
//// // => Ok(Parsed(["c"], [#("a", "b")]))
//// ```
////
//// The `pair` combinator takes two parsers and returns a new parser that runs the first parser and then the second
//// parser, returning both results as a tuple if both are successful.
////
//// To create parsers that are actually useful we need to be able to branch based on the input,
//// for that we can use the `alt` combinator:
////
//// ```gleam
//// let a_or_b_parser = alt(just("a"), just("b"))
//// parse_string("abc", a_or_b_parser)
//// // => Ok(Parsed(["b", "c"], "a"))
//// parse_string("bac", a_or_b_parser)
//// // => Ok(Parsed(["a", "c"], "b"))
//// ```
////
//// In cases where there are more than two options, we can use the `choice` combinator:
////
//// ```gleam
//// let a_b_or_c_parser = choice(just("a"), just("b"), just("c"))
//// parse_string("abc", a_b_or_c_parser)
//// // => Ok(Parsed(["b", "c"], "a"))
//// parse_string("bca", a_b_or_c_parser)
//// // => Ok(Parsed(["c", "a"], "b"))
//// parse_string("cab", a_b_or_c_parser)
//// // => Ok(Parsed(["a", "b"], "c"))
//// ```
////
//// The next crucial combinators are `many0` and `many1`.
//// They allow us to parse zero or more or one or more repetitions of a parser:
////
//// ```gleam
//// let abc0_parser = many0(a_b_or_c_parser)
//// parse_string("abc", abc0_parser)
//// // => Ok(Parsed([], ["a", "b", "c"]))
//// parse_string("cab", abc0_parser)
//// // => Ok(Parsed([], ["c", "a", "b"]))
//// parse_string("abcbcacab", abc0_parser)
//// // => Ok(Parsed([], ["a", "b", "c", "b", "c", "a", "c", "a", "b"]))
//// ```
////
//// Sometimes we want to parse something and then ignore the result,
//// for that we can use the `left` and `right` combinators.
////
//// In this example we parse the letter 'a' followed by the letter 'b' and ignore the result of the first parser:
////
//// ```gleam
//// let a_followed_by_b_parser = left(just("a"), just("b"))
//// parse_string("abc", a_followed_by_b_parser)
//// // => Ok(Parsed(["c"], "a"))
//// ```
////
//// The `right` combinator works the same way but ignores the result of the second parser.
////
//// ```gleam
//// let b_preceeded_by_a_parser = right(just("a"), just("b"))
//// parse_string("abc", b_preceeded_by_a_parser)
//// // => Ok(Parsed(["c"], "b"))
//// ```
////
//// These building blocks are enough to build many combinators that can parse complex data structures.
//// For example the `sep_by0` and `sep_by1` combinators which can be used to parse lists of items
//// separated by a delimiter.
////
//// ```
//// let comma_separated_letters = sep_by0(a_or_b_or_c_parser, just(","))
//// parse_string("a,b,c", comma_separated_letters)
//// // => Ok(Parsed([], ["a", "b", "c"]))
//// ```
////
//// ### Transforming the results
////
//// The `map` combinator can be used to transform the result of a parser.
//// It takes a parser and a function that takes the result of the parser and returns a new value.
////
//// Let's say we want to parse our abc letters and transform them into the following types:
////
//// ```gleam
//// type Letter {
//// A
//// B
//// C
//// Other(String)
//// }
////
//// type Letters {
//// Letters(List(Letter))
//// }
//// ```
////
//// First we need to define parsers for each letter:
////
//// ```gleam
//// let a_parser = map(just("a"), fn(_letter) { Letter.A })
//// let b_parser = map(just("b"), fn(_letter) { Letter.B })
//// let c_parser = map(just("c"), fn(_letter) { Letter.C })
//// ```
////
//// In our map function we ignore the input and return the corresponding letter. We will get to the `Other` case later.
////
//// Now that we have parsers that take a char and we can combine them as previously and then use the `map` combinator
//// to transform the result into `Letters`:
////
//// ```gleam
//// let letters_parser =
//// choice([a_parser, b_parser, c_parser])
//// |> map(Letters)
////
//// parse_string("abc", letters_parser)
//// // => Ok(Parsed([], Letters([Letter.A, Letter.B, Letter.C])))
//// ```
////
//// Let's add the `Other` case to our parser, for that we can use the satisfying combinator which takes a function
//// that returns a boolean indicating if the input matches or not. We can use it to check if the input is a letter
//// of the alphabet.
////
//// While we are at it, let's also use `to` instead of `map` to transform our a, b, and c parsers into the `Letter` type.
//// `to` is a shorthand for `map` where you want to ignore the input and just return a new value.
////
//// ```gleam
//// let a_parser = to(just("a"), Letter.A)
//// let b_parser = to(just("b"), Letter.B)
//// let c_parser = to(just("c"), Letter.C)
//// let other_parser = satisfying(fn(char) { is_alphabetic(char) })
////
//// let letters_parser =
//// choice([a_parser, b_parser, c_parser, other_parser])
//// |> map(Letters)
////
//// parse_string("abcd", letters_parser)
//// // => Ok(Parsed(["d"], Letters([Letter.A, Letter.B, Letter.C, Letter.Other("d")]))
//// ```
////
//// ### More examples
////
//// Please have a look at the tests for more complex examples, such as parsing JSON or Brainf*ck.
import gleam/string
import pears/input.{type Input}
/// Returned by a parser when it is successful. It contains the remaining input and the parsed value.
pub type Parsed(i, a) {
Parsed(input: Input(i), value: a)
}
pub type ParseError(i) {
ParseError(input: Input(i), expected: List(String))
}
/// The result of a parser. It is either a `Parsed` or a `ParseError`.
pub type ParseResult(i, a) =
Result(Parsed(i, a), ParseError(i))
/// The generic parser type. It's an alias for a function that takes an `Input(i)` and returns a `ParseResult(i, a)`.
pub type Parser(i, a) =
fn(Input(i)) -> ParseResult(i, a)
/// Helper function that takes a parser and an input and runs the parser on the input.
pub fn parse(i: List(i), p: Parser(i, a)) -> ParseResult(i, a) {
p(i)
}
/// Helper function that takes a string and a parser and runs the parser on the string.
pub fn parse_string(i: String, p: Parser(String, a)) -> ParseResult(String, a) {
i
|> string.to_graphemes()
|> parse(p)
}
pub fn ok(input: Input(i), value: a) -> ParseResult(i, a) {
Ok(Parsed(input, value))
}