Skip to content

RFC: APIs for recoverable failures #45080

@tkf

Description

@tkf

There have been discussions on API for recoverable failures #41966 #34821 #44397 albeit case-by-case basis. By "recoverable," it means that the caller program expects certain failure modes (which is documented as public API of the callee) and thus the author of the caller program is willing to handle "success" and "failure" paths efficiently. We need a basic protocol for such APIs since the compiler is not very eager on optimizing exception-based error handling 1 (and, as a result, e.g., it is hard to use in constrained run-time environments such as GPU devices). I suggest looking at these issues as a whole and trying to come up with a principled solution. This was also discussed in #43773 but no concrete solution was proposed. This issue discusses a two-part RFC that is aiming at landing a concrete solution.

The first part of the RFC is to create simple wrapper types like Some but conveys "success" and "failure" semantics in some sense. Namely, I suggest adding

struct Ok{T}
    value::T
end

struct Err{T}
    value::T
end

A lot of interesting mechanisms can be built around them that may even require changing the struct itself (e.g., Try.jl). To maximize what's possible in 1.x time-frame, I suggest only exporting abstract types from Base and putting concrete structs in Base.Experimental.

The second part of the RFC is to add some Union{Ok,Err}-valued functions to address issues like #41966, #34821, and #44397. Some possible APIs include:

API Success Failure
tryget(dict, key) Ok(key′ => value) 2 Err(KeyError(key))
trysetwith!(f, dict, key) Ok(key′ => f()) 2 Ok(key′ => dict[key]) 2
tryset!(dict, key, value) Ok(key′ => value′) 2 Ok(key′ => dict[key]) 2
tryput!(xs, x) Ok(x) Err(ClosedError(xs)) etc.
trytake!(xs) Ok(x) Err(ClosedError(xs)) etc.
tryfetch(xs) Ok(x) Err(ClosedError(xs)) etc.
trypop!(xs) Ok(x) Err(EmptyError(xs)) etc.
trypopfirst!(xs) Ok(x) Err(EmptyError(xs)) etc.
trypush!(xs, x) Ok(x′) (Ok(xs)?) 2 Err(ClosedError(xs)) etc.
trypushfirst!(xs, x) Ok(x′) (Ok(xs)?) 2 Err(ClosedError(xs)) etc.
trygetonly(xs) Ok(xs) Err(EmptyError(xs))

(Concrete implementations of tryget(dict, key), trysetwith!(f, dict, key), and tryset!(dict, key, value) 3 can be found in PreludeDicts.jl. Some of other functions are implemented in TryExperimental.jl.)

I suggest promoting try prefix for this "calling convention." This is similar to the ! suffix convention that hints mutation in some sense.

Of course, we already have a couple of functions with try prefix in Base (but fortunately not in stdlib) APIs:

julia> collect(Iterators.filter(startswith("try"), Iterators.map(string, Iterators.flatten(Iterators.map(names  Base.require, collect(keys(Base.loaded_modules)))))))
2-element Vector{String}:
 "trylock"
 "tryparse"

We can leave them as-is but it may also be reasonable to introduce and promote aliases (as we obviously cannot remove them during 1.x time frame). For example, trylock could be called racylock or lock_nowait. ConcurrentUtils.jl uses race_ prefix for racy and non-deterministic APIs in general including trylock. That said, since trylock is a pretty standard terminology, I understand that people may want to keep it. tryparse could be renamed to maybeparse. However, rather than simple rename, it may be better to clean up the API so that it is possible to support, e.g., maybeparse(Union{Int,Nothing}, "nothing")::Union{Nothing,Some{<:Union{Int,Nothing}}} == Some(nothing) #43746.

The "exit condition" of the RFC is not implementing the above APIs in the table. Rather, I think it'd be a good idea to start with one of #41966, #34821, or #44397. However, it seems to be a better idea to try arriving at a naming convention before tackling individual problems. That said, I think it is also a reasonable end result to decide not using any naming scheme for this type of API.

In summary, I think it'd be great if we can decide (at least):

  • Adding the Ok and Err types in Base or not
  • Using a naming convention for this kind of API or not

Footnotes

  1. For more discussion on the co-existence of two error handling mechanisms, I included a section in Try.jl README: When to throw? When to return?

  2. indicates that the returned value include the object that is actually stored in the container. For example, tryset!(dict, key, value) may return Ok(key′ => value′) with key !== key′ and value !== value′ if key and value are converted. 2 3 4 5 6 7

  3. Note that tryset!(dict, key, value)/trysetwith!(f, dict, key) are the "failable" counter parts of get!(dict, key, value)/get!(f, dict, key) respectively. I swapped get with set since (1) it clarifies the implication on the failure path and (2) adding the with suffix as in mergewith! is more natural.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions