Hi! Working through the Advent of Code is my first experience with Roc tasks (everything else I've done has been repl stuff). I do have experience working with commands/tasks in Elm, though that does not have tag unions, and so that knowledge doesn't seem to transfer much.
Much of what I'm running into appears to be the same as in this thread, though I'm running into some conceptual difficulty on a number of points, particularly regarding basic-cli:
I do not know if different OS platforms require syscalls to retrieve program arguments, or if those syscalls could be expected to fail. On Linux, at least, afaik, program arguments (and environment variables) are loaded into the main thread's stack prior to handing off control to the program, and thus argument reading (nor env var reading) are not expected to fail.
Needing to deal with a Task to read what is essentially an in-memory array of strings is an annoyance. Is there a way for a Roc platform to provide constant data to the Roc application? Granted, the Str and List formats are likely not compatible (null terminated instead of length-encoded), but it seems like there could be value in Roc providing platforms with a way to intern a whole-of-program-lifecycle (List Str) that stores Roc-formatted lengths, but otherwise points at the underlying string data.
Given the following program:
main = await Stdin.line Stdout.line
When running it, if Ctrl-D is used (on a Unix-like system at least), the following panic occurs:
thread 'main' panicked at 'called
Option::unwrap()
on aNone
value', src/lib.rs:310:45
note: run withRUST_BACKTRACE=1
environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5
In particular, this means I can't pipe unmodified Advent of Code inputs to stdin, which would have been rather convenient, due to the lack of the ability to detect EOF. In general, the inability to detect EOF is particularly limiting for a Stdin implementation. Perhaps a variant function Stdlin.lineChecked
could be exposed to cover this case, leaving Stdin.line
for cases where the caller assumes input will be available? fwiw, detecting file-closed or broken-pipe conditions is also important for writing efficient, graceful stream processors: imagine a Roc program producing millions of lines of output, but being passed to the head
utility: ideally, the Roc program should be able to detect the inability to write and save work by exiting early.
This Task.await call produces:
Task (List Str) [FileReadErr Path.Path InternalFile.ReadErr,
FileReadUtf8Err Path.Path [BadUtf8 Utf8ByteProblem Nat]]
But the type annotation on readLines says it should be:
Task (List Str) *
I have a few questions:
Are there effective techniques for ensuring that all errors can be handled _within_ a function that uses Task.await
? It seems that await
makes it so that the remainder of the function is unable to handle the initial error, and that Task.attempt
may be needed to handle errors within the _same_ function.
FileReadErr
exposes mention of an InternalFile.ReadErr
. Is this intentional? Is this something we can reference and inspect in the calling application? Use of Internal
in exposed error messages and APIs, at least for me, decreases confidence in the maturity of the system (though I realize it's a work-in-progress).
Does FileReadUtf8Err
need to bundle BadUtf8
, and in turn, does BadUtf8
need to bundle Utf8ByteProblem
? Could FileReadErr
just directly bundle BadUtf8
as one of its tag arguments instead of introducing a separate top-level error?
With basic-cli, something I've run into regularly is a compiler error to the effect of:
Something is off with the body of the main definition
This Task.await call produces:
Task {} [SomeFailure]
But the type annotation on main says it should be:
Task {} *
(This is a fake example synthesized using Task.fail
, though I have seen real cases of this).
It's clear that I need to handle every error, either individually, or all at once, though as mentioned before, this either means splitting error handling out of the function generating the tasks (perhaps this is best practice?), or switching await
to attempt
, which is a bit of a pain.
I'm hoping to hear about techniques for handling errors cleanly.
Although iterating on code can mitigate this somewhat, it can be frustrating to end up in cases where if
/else
/when
cases nest 4+ levels deep. The mitigation I referred to often involves using Result.map
, Task.onFail
, etc, which makes the code more succinct, but at the expense of making it less readable to inexperienced Roc/functional programmers (further made worse by the naming similarities between the conceptual similarities between onFail
, mapErr
, etc, and the frequent need, at least for me, to consult type declarations in documentation).
Are there in-language ways (using the core syntax rather than functions) to minimize this nesting? I'm sure this will be contentious, but do we really need else
? In imperative languages, such as Python, minimizing indentation is frequently considered a readability boon (i.e. instead of an else
(assuming the else
would've spanned the remainder of the function), just use a return
within the if
. The cognitive load is decreased because that technique essentially signals "we're done with these variables or this case, and we don't need to worry about this condition going forward" (the cognitive load aspect doesn't strictly apply to Roc because there can't be anything _after_ an else
). Also, this reduces the likelihood of horizontal scrolling when viewing source code, which certainly does apply to Roc.
Following the same trend of reducing indentation by allowing implicit assignment without a let
, since Roc does not allow any code within a function to exist _after_ an else
, can we permit an unpaired if
, where the unindented code that follows forms an _implicit_ else
? For example, could these be equivalent? What would be the downsides?
if x then
x
else
y = someExpensiveExpr
if y then
y
else
someOtherExpensiveExpr
if x then
x
y = someExpensiveExpr
if y then
y
someOtherExpensiveExpr
(Any de-indentation would imply an early return, or alternately, the grammar could rewrite a de-indentation to be semantically the same as a re-indentation under an else
)
Similarly, could a de-indentation following a when
be equivalent to a _
case?
Alternatively, if the Roc language could guarantee that expressions will only be computed at the point of their first use, I might be willing to write:
y = someExpensiveExpr
z = someOtherExpensiveExpr
if x then
x
else if y then
y
else
z
In cases where we just don't care to handle errors explicitly (such as in a time-sensitive Advent of Code competition), but also In the interest of being able to report errors cheaply, is there, or could there be some way for the language to intern the primary/top-level tag name in the binary, and then provide a function which can convert any tag to its name? There could be a lot of utility in being able to print out the difference between a FileNotFound style error vs an Encoding error, even if we can't see the tag argument information.
Alternatively, could there be an ability that error tags implement which allow them to render themselves as a Str?
Handling errors explicitly is fine and to be encouraged for productionized systems, but the error handling trade-offs I was able to infer at least in Roc today limit its utility for developing throwaway scripts.
Hi @Kevin Gillette, I appreciate the detailed feedback! It would be great if you could split the sections up over multiple topics, that will make discussion easier.
I'll do that, thanks
Last updated: Jul 05 2025 at 12:14 UTC