Skip to content

Commit

Permalink
Java's throws-like usage (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkf authored Jan 8, 2022
1 parent 362f834 commit 544f3d6
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 2 deletions.
74 changes: 73 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,77 @@ julia> @code_typed(Try.first(Int[]))[2] # both are possible for an array
Union{Ok{Int64}, Err{BoundsError}}
```

### Constraining returnable errors

We can use the return type conversion `function f(...)::ReturnType ... end` to
constrain possible error types. This is similar to the `throws` keyword in Java.

This can be used for ensuring that only the expected set of errors are returned
from Try.jl-based functions. In particular, it may be useful for restricting
possible errors at an API boundary. The idea is to separate "call API" `f` from
"overload API" `__f__` such that new methods are added to `__f__` and not to
`f`. We can then wrap the overload API function by the call API function that
simply declare the return type:

```Julia
f(args...)::Result{Any,PossibleErrors} = __f__(args...)
```

(Using type assertion as in `__f__(args...)::Result{Any,PossibleErrors}` also
works in this case.)

Then, the API specification of `f` can include the overloading instruction
explaining that method of `__f__` should be defined and enumerate allowed set of
errors.

Here is an example of providing the call API `tryparse` with the overload API
`__tryparse__` wrapping `Base.tryparase`. In this toy example, `__tryparse__`
can return `InvalidCharError()` or `EndOfBufferError()` as an error value:

```julia
using Try

struct InvalidCharError <: Exception end
struct EndOfBufferError <: Exception end

const ParseError = Union{InvalidCharError, EndOfBufferError}

tryparse(T, str)::Result{T,ParseError} = __tryparse__(T, str)

function __tryparse__(::Type{Int}, str::AbstractString)
isempty(str) && return Err(EndOfBufferError())
Ok(@something(Base.tryparse(Int, str), return Err(InvalidCharError())))
end

tryparse(Int, "111")

# output
Try.Ok: 111
```

```julia
tryparse(Int, "")

# output
Try.Err: EndOfBufferError()
```

```julia
tryparse(Int, "one")

# output
Try.Err: InvalidCharError()
```

Constraining errors can be useful for generic programming if it is desirable to
ensure that error handling is complete. This pattern makes it easy to *report
invalid errors directly to the programmer* (see [When to `throw`? When to
`return`?](#when-to-throw-when-to-return)) while correctly implemented methods
do not incur any run-time overheads.

See also:
[julep: "chain of custody" error handling · Issue #7026 · JuliaLang/julia](https://github.com/JuliaLang/julia/issues/7026)

## Discussion

Julia is a dynamic language with a compiler that can aggressively optimize away
Expand Down Expand Up @@ -297,4 +368,5 @@ the whole program is not an option. Thus, it is crucial that it is possible to
recover even from an out-of-contract error in Julia. Such a language construct
is required for building programming tools such as REPL and editor plugins can
use it. In summary, `return`-based error reporting is adequate for recoverable
errors and `throw`-based error reporting is adequate for unrecoverable errors.
errors and `throw`-based error reporting is adequate for unrecoverable (i.e.,
programmer's) errors.
2 changes: 1 addition & 1 deletion src/Try.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ end
const ConcreteOk{T} = ConcreteResult{T,Union{}}
const ConcreteErr{E<:Exception} = ConcreteResult{Union{},E}

const Result{T,E} = Union{ConcreteResult{T,E},DynamicResult{T,E}}
const Result{T,E} = Union{ConcreteResult{<:T,<:E},DynamicResult{<:T,<:E}}

function throw end

Expand Down
4 changes: 4 additions & 0 deletions src/core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ end

Base.convert(::Type{Ok{T}}, ok::Ok) where {T} = Ok{T}(ok.value)
Base.convert(::Type{Err{E}}, err::Err) where {E} = Err{E}(err.value)
# An interesting approach may be to simply throw the `err.value` if it is not a
# subtype of `E`. It makes the error value propagation pretty close to the
# chain-of-custody Julep. Maybe this should be done only when the destination
# type is `AbstractResult{<:Any,E′}` s.t. `!(err.value isa E′)`.

_concrete(result::Ok) = _ConcreteResult(Try.oktype(result), Union{}, result)
_concrete(result::Err) = _ConcreteResult(Union{}, Try.errtype(result), result)
Expand Down

0 comments on commit 544f3d6

Please sign in to comment.