You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
IO/done_on_err was added to the prelude with the intent of being a function that stops the current block of monadic IO actions, similar to how ? works in rust for example.
The idea is that if the result of any of those actions fails, we bail out early and return the error. This is meant as a simpler way of writing nested matches that in the future we could transform into a syntax sugar.
After executing a Call, it's result is passed on to cont until it's a Done, in which case the program stops.
The second way of sequencing is using IO/bind. It completely executes a chain of Calls and then passes the result to the next IO chain.
What IO/done_on_err currently does is discard the continuations of a Call in case the returned value was an error. It does not stop the execution of the bind continuation because it can only act on the IO result it's wrapping.
In the read_file example above, done_on_error is simply not doing anything since all the values it wraps already have only a single action in their Call chain. The result is that a value of type Result is incorrectly passed to the next actions and everything will break due to type errors.
As a result, all the tests that should handle an IO failure are giving incorrect result and most of the IO functions are broken on failure.
Here are some alternatives to the done_on_error approach:
Using a try_bind function that does the same thing as bind but is meant for stopping early
IO/try_bind (IO/Done (Result/Ok x)) b = (undefer b x)
IO/try_bind (IO/Done (Result/Err x)) b = (IO/Done (Result/Err x))
IO/try_bind (IO/Call magic fn args cont) b = (IO/call magic fn args @x (IO/try_bind (cont x) b))
It could for example be used in a try block of some sorts that uses try_bind instead of bind, or we could individually write a "try ask" syntax like a? <- IO/action_that_can_fail.
Another approach would be using a function that wraps around the next action like
IO/and (a: Result(A, E), b: A -> IO(Result(B, E))) -> IO(Result(B, E))
IO/and (Result/Ok a) b = (undefer b a)
IO/and (Result/Err e) _ = (IO/wrap (Result/Err e))
Although this would work, it would be pretty bad to write. Potentially there could be some syntax sugar or better alternative function we could use, but with and the read_file example would become something like this (haven't tested this, probably something wrong), which is not legible:
def IO/FS/read_file(path: String) -> IO(Result(List(u24), u24)):
with IO:
fd <- IO/FS/open(path, "r")
bytes_fd <- IO/and(fd, lambda fd: IO/map(IO/FS/read_to_end(fd), Result/map(@x (x, fd))))
# would need a way to access the tuple fields in an expression
res <- IO/and(bytes_fd, lambda bytes_fd: IO/map(IO/FS/close(bytes_fd.1), Result/map(@x (x, bytes_fd.0))))
return IO/and(res, @res wrap(res.0))
I can't think of any good ways of doing a function like this in the imperative syntax.
There could be a way of composing two monads. I haven't looked into it, but we could create a monad that is the composite of IO with Result. Seems totally doable, but I don't know how to do this in a way that is not confusing for most users.
The final option would be to change the IO type to have an Error variant that returns early.
Then, instead of raw IO calls returning IO(Result(A, IOError(B)), they'd simply return IO(A, B) where A is the type of the expected value and B is the type of the error.
This option is similar to how haskell does it, for example. It would also require that we change the error type returned by HVM
I like the last two options because they don't require new syntax for making the IO error handling bearable for users, but I'm open to all ideas.
Although we technically could do the initial IO release with things how they are now, this issue may require very large changes to all the IO in the future, so it hsould probably go in the IO v0 milestone.
The advantage of having an explicit "try" block is that it becomes easy to add error/exit handlers to do things like close files.
In an imperative syntax, to do that without something like try ... finally you'd need to declare a local function that does just the part that fails with the resource and them call it, catch the potential errors and release the resources.
Of course, a catch or a finally could be added as syntax to any of the alternative I proposed above.
We could add a syntax for mapping the continuation of a bind. I think it could work both before and after executing the bind.
# Syntax-wise this is not great, but I'm trying to show the idea.defIO/FS/read_file(path):
withIO:
fd<-IO/FS/open(path, "r") |>IO/done_on_errbytes<-IO/FS/read_to_end(fd) |>IO/done_on_err*<-IO/FS/close(fd) |>IO/done_on_errreturnwrap(bytes)
It's a function that receives the result of the action and the continuation of the bind and returns a new continuation that does something in the middle.
Reproducing the behavior
IO/done_on_err
was added to the prelude with the intent of being a function that stops the current block of monadic IO actions, similar to how?
works in rust for example.For example, this is how
IO/FS/read_file
uses it:The idea is that if the result of any of those actions fails, we bail out early and return the error. This is meant as a simpler way of writing nested matches that in the future we could transform into a syntax sugar.
The problem is that with the way it's implemented (and using any similar strategy) it does not exit early from actions sequenced via an
IO/bind
.There are two way of sequencing IO actions. One is by setting up a continuation inside the
IO
value itself.The definition of the IO type is
After executing a
Call
, it's result is passed on tocont
until it's aDone
, in which case the program stops.The second way of sequencing is using
IO/bind
. It completely executes a chain ofCall
s and then passes the result to the next IO chain.What
IO/done_on_err
currently does is discard the continuations of aCall
in case the returned value was an error. It does not stop the execution of the bind continuation because it can only act on the IO result it's wrapping.In the
read_file
example above,done_on_error
is simply not doing anything since all the values it wraps already have only a single action in theirCall
chain. The result is that a value of typeResult
is incorrectly passed to the next actions and everything will break due to type errors.As a result, all the tests that should handle an IO failure are giving incorrect result and most of the IO functions are broken on failure.
Here are some alternatives to the
done_on_error
approach:Using a
try_bind
function that does the same thing asbind
but is meant for stopping earlyIt could for example be used in a
try
block of some sorts that usestry_bind
instead ofbind
, or we could individually write a "try ask" syntax likea? <- IO/action_that_can_fail
.Another approach would be using a function that wraps around the next action like
Although this would work, it would be pretty bad to write. Potentially there could be some syntax sugar or better alternative function we could use, but with
and
theread_file
example would become something like this (haven't tested this, probably something wrong), which is not legible:I can't think of any good ways of doing a function like this in the imperative syntax.
There could be a way of composing two monads. I haven't looked into it, but we could create a monad that is the composite of IO with Result. Seems totally doable, but I don't know how to do this in a way that is not confusing for most users.
The final option would be to change the
IO
type to have anError
variant that returns early.Then, instead of raw IO calls returning
IO(Result(A, IOError(B))
, they'd simply returnIO(A, B)
whereA
is the type of the expected value andB
is the type of the error.This option is similar to how haskell does it, for example. It would also require that we change the error type returned by HVM
I like the last two options because they don't require new syntax for making the IO error handling bearable for users, but I'm open to all ideas.
Although we technically could do the initial IO release with things how they are now, this issue may require very large changes to all the IO in the future, so it hsould probably go in the IO v0 milestone.
System Settings
Bend commit 8246a50
Additional context
No response
The text was updated successfully, but these errors were encountered: