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.
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 }
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)\""
Very nice!
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