Stream: advent of code

Topic: 2020 Day 1


view this post on Zulip Luke Boswell (Apr 07 2023 at 08:17):

I have gone through another AoC puzzle to see if there were things I would do differently now I have learnt a bit more about Roc. I'm pretty happy with the end result. Note my focus was on how to use Roc and not really algorithm performance.

This is the code AoC 2020 Problem 1 I am referring to below.

There are two main things that I think worth sharing from my exploration.

Parsing Numbers using Decode Ability

From my experience working on the Json decoder I think this is a handy way to parse the file input bytes. Basically, just recursively parse a number from the list of bytes and build up a list. This can be extended to support other Decode.custom decoders too. An alternative method to using a Parser and combinators.

Also I think it should be reasonably efficient now with seamless slices.

# Should probably have used `numbers: List.withCapacity []` here
{ numbers, rest } = parseNumbers { numbers: [], rest: input }

parseNumbers = \{ numbers, rest } ->
    if List.isEmpty rest then
        { numbers, rest }
    else
        decodeResult : Decode.DecodeResult U64
        decodeResult = Decode.fromBytesPartial rest Json.fromUtf8

        when decodeResult.result is
            Ok n -> parseNumbers { numbers: List.append numbers n, rest: decodeResult.rest }
            Err _ -> parseNumbers { numbers, rest: List.dropFirst rest }

Task API and Error Handling

I wanted to revisit the Task API and figure out a nicer way to handle errors, rather than just crashing onFail which was what I had defaulted to.

I found that using a tag union to group task errors at the level you care about is really nice. You can provide good descriptions for what the error is so you can handle it effectively.

If you separate the tasks into distinct functions it is easier to add type annotions for the detail you care about. You can ignore lower level errors, or include additional information such as the "path" for an invalid file etc.

TaskErrors : [InvalidArg, InvalidFile Str]

readPath : Task.Task Str TaskErrors

readFile : Str -> Task.Task (List U8) TaskErrors

This simplifies the flow of tasks and makes it easy to use backpassing syntax when using the tasks and handling errors.

main =
    task =
        path <- readPath |> Task.await
        fileBytes <- readFile path |> Task.await

        [
            part1 "Part 1 Sample" sampleBytes,
            part1 "Part 1 File" fileBytes,
            part2 "Part 2 Sample" sampleBytes,
            part2 "Part 2 File" fileBytes,
        ]
        |> List.keepOks \x -> x
        |> Str.joinWith "\n"
        |> Task.succeed

    taskResult <- Task.attempt task

    when taskResult is
        Ok answers -> Stdout.line answers
        Err InvalidArg -> Stderr.line "Error: expected arg file.roc -- path/to/input.txt"
        Err (InvalidFile path) -> Stderr.line "Error: couldn't read input file at \"\(path)\""

view this post on Zulip Brian Carroll (Apr 07 2023 at 10:33):

Very nice!

view this post on Zulip Brian Carroll (Apr 07 2023 at 10:54):

You mentioned List.withCapacity. Its argument is actually a number - how many elements you want to reserve initial capacity for.

Here I think you'd guess the initial capacity based on the length of the input string divided by... some made-up constant. Maybe 4? It's not worth being accurate by counting the commas because then you're not saving time!

If that guess is too small, it might have to resize the list, but hopefully fewer resizes than if you hadn't given a guess. If the guess is too big, you waste some memory. Either way, the code still works.

If you don't do any of this, the default behaviour is pretty good anyway.


Last updated: Jul 06 2025 at 12:14 UTC