Skip to content

Commit

Permalink
upgrade RecordBuilder example
Browse files Browse the repository at this point in the history
  • Loading branch information
lukewilliamboswell committed Jan 10, 2025
1 parent ef6ada7 commit 0ed2054
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 59 deletions.
56 changes: 36 additions & 20 deletions examples/RecordBuilder/DateParser.roc
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,54 @@ ParserGroup a := List Str -> Result (a, List Str) ParserErr

parse_with : (Str -> Result a ParserErr) -> ParserGroup a
parse_with = \parser ->
@ParserGroup \segments ->
when segments is
[] -> Err OutOfSegments
[first, .. as rest] ->
parsed = parser? first
Ok (parsed, rest)
@ParserGroup(
\segments ->
when segments is
[] -> Err(OutOfSegments)
[first, .. as rest] ->
parsed = parser(first)?
Ok((parsed, rest)),
)

chain_parsers : ParserGroup a, ParserGroup b, (a, b -> c) -> ParserGroup c
chain_parsers = \@ParserGroup first, @ParserGroup second, combiner ->
@ParserGroup \segments ->
(a, after_first) = first? segments
(b, after_second) = second? after_first
chain_parsers = \@ParserGroup(first), @ParserGroup(second), combiner ->
@ParserGroup(
\segments ->
(a, after_first) = first(segments)?
(b, after_second) = second(after_first)?

Ok (combiner a b, after_second)
Ok((combiner(a, b), after_second)),
)

build_segment_parser : ParserGroup a -> (Str -> Result a ParserErr)
build_segment_parser = \@ParserGroup parser_group ->
build_segment_parser = \@ParserGroup(parser_group) ->
\text ->
segments = Str.splitOn text "-"
(date, _remaining) = parser_group? segments
segments = Str.split_on(text, "-")
(date, _remaining) = parser_group(segments)?

Ok date
Ok(date)

expect
date_parser =
{ chain_parsers <-
month: parse_with Ok,
day: parse_with Str.toU64,
year: parse_with Str.toU64,
month: parse_with(Ok),
day: parse_with(Str.to_u64),
year: parse_with(Str.to_u64),
}
|> build_segment_parser

date = date_parser "Mar-10-2015"
date_parser("Mar-10-2015") == Ok({ month: "Mar", day: 10, year: 2015 })

date == Ok { month: "Mar", day: 10, year: 2015 }
expect
date_parser =
build_segment_parser(chain_parsers(
parse_with(Ok),
chain_parsers(
parse_with(Str.to_u64),
parse_with(Str.to_u64),
\day, year -> (day, year),
),
\month, (day, year) -> { month, day, year },
))

date_parser("Mar-10-2015") == Ok({ month: "Mar", day: 10, year: 2015 })
79 changes: 40 additions & 39 deletions examples/RecordBuilder/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Record Builder
# Record Builder

Record builders are a syntax sugar for sequencing actions and collecting the intermediate results as fields in a record. All you need to build a record is a `map2`-style function that takes two values of the same type and combines them using a provided combiner fnuction. There are many convenient APIs we can build with this simple syntax.

Expand All @@ -24,43 +24,42 @@ It's useful to visualize our desired result. The record builder pattern we're ai

```roc
expect
dateParser : ParserGroup Date
dateParser =
{ chainParsers <-
month: parseWith Ok,
day: parseWith Str.toU64,
year: parseWith Str.toU64,
date_parser =
{ chain_parsers <-
month: parse_with(Ok),
day: parse_with(Str.to_u64),
year: parse_with(Str.to_u64),
}
|> buildSegmentParser
|> build_segment_parser
date = dateParser "Mar-10-2015"
date == Ok { month: "Mar", day: 10, year: 2015 }
date_parser("Mar-10-2015") == Ok({ month: "Mar", day: 10, year: 2015 })
```

This generates a record with fields `month`, `day`, and `year`, all possessing specific parts of the provided date. Note the slight deviation from the conventional record syntax, with the `chainParsers <-` at the top, which is our `map2`-style function.
This generates a record with fields `month`, `day`, and `year`, all possessing specific parts of the provided date. Note the slight deviation from the conventional record syntax, with the `chain_parsers <-` at the top, which is our `map2`-style function.

## Under the Hood

The record builder pattern is syntax sugar which converts the preceding into:

```roc
expect
dateParser : ParserGroup Date
dateParser =
chainParsers
(parseWith Ok)
(chainParsers
(parseWith Str.toU64)
(parseWith Str.toU64)
(\day, year -> (day, year))
)
(\month, (day, year) -> { month, day, year })
date_parser =
build_segment_parser(chain_parsers(
parse_with(Ok),
chain_parsers(
parse_with(Str.to_u64),
parse_with(Str.to_u64),
\day, year -> (day, year),
),
\month, (day, year) -> { month, day, year },
))
date_parser("Mar-10-2015") == Ok({ month: "Mar", day: 10, year: 2015 })
```

In short, we chain together all pairs of field values with the `map2` combining function, pairing them into tuples until the final grouping of values is structured as a record.

To make the above possible, we'll need to define the `parseWith` function that turns a parser into a `ParserGroup`, and the `chainParsers` function that acts as our `map2` combining function.
To make the above possible, we'll need to define the `parse_with` function that turns a parser into a `ParserGroup`, and the `chain_parsers` function that acts as our `map2` combining function.

## Defining Our Functions

Expand All @@ -77,30 +76,32 @@ parseWith = \parser ->
Ok (parsed, rest)
```

This parses the first segment available, and returns the parsed data along with all remaining segments not yet parsed. We could already use this to parse a single-segment string without even using a record builder, but that wouldn't be very useful. Let's see how our `chainParsers` function will manage combining two `ParserGroup`s in serial:
This parses the first segment available, and returns the parsed data along with all remaining segments not yet parsed. We could already use this to parse a single-segment string without even using a record builder, but that wouldn't be very useful. Let's see how our `chain_parsers` function will manage combining two `ParserGroup`s in serial:

```roc
chainParsers : ParserGroup a, ParserGroup b, (a, b -> c) -> ParserGroup c
chainParsers = \@ParserGroup first, @ParserGroup second, combiner ->
@ParserGroup \segments ->
(a, afterFirst) = first? segments
(b, afterSecond) = second? afterFirst
Ok (combiner a b, afterSecond)
chain_parsers : ParserGroup a, ParserGroup b, (a, b -> c) -> ParserGroup c
chain_parsers = \@ParserGroup(first), @ParserGroup(second), combiner ->
@ParserGroup(
\segments ->
(a, after_first) = first(segments)?
(b, after_second) = second(after_first)?
Ok((combiner(a, b), after_second)),
)
```

Just parse the two groups, and then combine their results? That was easy!

Finally, we'll need to wrap up our parsers into one that breaks a string into segments and then applies our parsers on said segments. We can call it `buildSegmentParser`:
Finally, we'll need to wrap up our parsers into one that breaks a string into segments and then applies our parsers on said segments. We can call it `build_segment_parser`:

```roc
buildSegmentParser : ParserGroup a -> (Str -> Result a ParserErr)
buildSegmentParser = \@ParserGroup parserGroup ->
build_segment_parser : ParserGroup a -> (Str -> Result a ParserErr)
build_segment_parser = \@ParserGroup(parser_group) ->
\text ->
segments = Str.splitOn text "-"
(date, _remaining) = parserGroup? segments
segments = Str.split_on(text, "-")
(date, _remaining) = parser_group(segments)?
Ok date
Ok(date)
```

Now we're ready to use our parser as much as we want on any input text!
Expand All @@ -116,7 +117,7 @@ file:DateParser.roc
Code for the above example is available in `DateParser.roc` which you can run like this:

```sh
% roc test IDCounter.roc
% roc test DateParser.roc

0 failed and 1 passed in 190 ms.
0 failed and 2 passed in 190 ms.
```

0 comments on commit 0ed2054

Please sign in to comment.