-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Description
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
endA 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
OkandErrtypes inBaseor not - Using a naming convention for this kind of API or not
Footnotes
-
For more discussion on the co-existence of two error handling mechanisms, I included a section in Try.jl README: When to
throw? When toreturn? ↩ -
′indicates that the returned value include the object that is actually stored in the container. For example,tryset!(dict, key, value)may returnOk(key′ => value′)withkey !== key′andvalue !== value′ifkeyandvalueareconverted. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Note that
tryset!(dict, key, value)/trysetwith!(f, dict, key)are the "failable" counter parts ofget!(dict, key, value)/get!(f, dict, key)respectively. I swappedgetwithsetsince (1) it clarifies the implication on the failure path and (2) adding thewithsuffix as inmergewith!is more natural. ↩