Usage
The Result type
Fundamentally, we have the two types Ok{T}
and Err{E}
, each just being wrapper types of a T
or E
, respectively. These represent a successful value of type T
or an error of type E
. For example, a function returning either a string or a 32-bit integer error code could return a Result{String, Int32}
.
Ok
and Err
are not supposed to be instantiated directly (and indeed, cannot be easily instantiated). Instead, they only exist as fields of a Result{T, E}
type, which contains a field typed Union{Ok{T}, Err{E}}
. You can construct it like this:
julia> Result{String, Int}(Ok("Nothing went wrong"))
Result{String, Int64}(Ok("Nothing went wrong"))
Thus, a Result{T, E}
represents either a successful creation of a T
or an error of type E
.
Option
Option{T}
is an alias for Result{T, Nothing}
, and is easier to work with fully specifying the parameters of Result
s. Option
is useful when the error state do not need to store any information besides the fact than an error occurred. Option
s can be conventiently created with two helper functions some
and none
:
julia> some(1) === Result{typeof(1), Nothing}(Ok(1))
true
julia> none(Int) === Result{Int, Nothing}(Err(nothing))
true
If you want an abstractly parameterized Option
, you can construct it directly like this:
julia> Option{Integer}(some(1))
Option{Integer}(some(1))
ResultConstructor
Behind the scenes, calling Err(x)
or Ok(x)
creates an instance of the non-exported type ResultConstructor
.
julia> Err(1)
ErrorTypes.ResultConstructor{Int64, Err}(1)
julia> Ok(1)
ErrorTypes.ResultConstructor{Int64, Ok}(1)
The only purpose of ResultConstructor
is to create Result
s with the correct parameters, and to allow conversions of carefully selected types. The user does not need to think much about ResultConstructor
, but if ErrorTypes
is abused, this type can show up in the stacktraces.
none
by itself is a constant for ErrorTypes.ResultConstructor{Nothing, Err}(nothing)
- we will come back to why this is particularly convenient.
Basic usage
Always typeassert any function that returns an error type. The whole point of ErrorTypes is to encode error states in return types, and be specific about these error states. While ErrorTypes will technically work fine without function annotations, it makes everything easier, and I highly recommend annotating return types:
Do this:
invert(x::Integer)::Option{Float64} = iszero(x) ? none : some(1/x)
And not this:
invert(x::Integer) = iszero(x) ? none(Float64) : some(1/x)
Note in the first example that none
can be returned, which was a generic instance of ResultConstructor
. One purpose of ResultConstructor
is that annotating the return type will cause this generic none
to automatically convert to Option{Float64}
. Similarly, one can also use a typeassert to ease in the construction of Result
:
function get_length(x)::Result{Int, Base.IteratorSize}
isz = Base.IteratorSize(x)
if isa(isz, Base.HasShape) || isa(isz, Base.HasLength)
return Ok(Int(length(x)))
else
return Err(isz)
end
end
In this example, Ok(Int(length(x))
returns a ResultConstructor{Int, Ok}
, which can be converted to the target Result{Int, Base.IteratorSize}
. Similarly, the Err(isz)
creates a ResultConstructor{Base.IteratorSize, Err}
, which can likewise be converted.
Conversion rules
Error types can only convert to each other in certain circumstances. This is intentional, because type conversion is a major source of mistakes.
- A
Result{T, E}
can be converted toResult{T2, E2}
iffT <: T2
andE <: E2
, i.e. you can always convert to a "larger" Result type or to its own type. - A
ResultConstructor{T, Ok}
can be converted toResult{T2, E}
ifT <: T2
. - A
ResultConstructor{E, Err}
can be converted toResult{T, E2}
ifE <: E2
.
The first rule merely state that a Result
can be converted to another Result
if both the success parameter (Ok{T}
) and the error parameter (Err{E}
) error types are a supertype. It is intentionally NOT possible to e.g. convert a Result{Int, String}
containing an Ok
to a Result{Int, Int}
, even if the Ok
value contains an Int
which is allowed in both of the Result
types. The reason for this is that if it was allowed, whether or not conversions threw errors would depend on the value of an error type, not the type. This type-unstable behaviour would defeat idea behind this package, namely to present edge cases as types, not values.
The next two rules state that ResultConstructors
have relaxed this requirement, and so a ResultConstructors
constructed from an Ok
or Err
can be converted if only the Ok{T}
or the Err{E}
parameter, respectively, is a supertype, not necessarily both parameters.
There is one last type, ResultConstructor{T, Union{}}
, which is even more flexible in how it converts. This is created by the @?
macro, discussed next.
@?
If you make an entire codebase of functions returning Result
s, it can get bothersome to constantly check if function calls contain error values and propagate those error values to their callers. To make this process easier, use the macro @?
, which automatically propagates any error values. If this is applied to some expression x
evaluating to a Result
containing a success value (i.e. Ok{T}
), the macro will evaluate to the inner wrapped value:
julia> @? Result{String, Int}(Ok("foo"))
"foo"
However, if x
evaluates to an error value Err{E}
, the macro creates a ResultConstructor{E, Union{}}
, let's call it y
, and evaluates to return y
. In this manner, the macro means "unwrap the value if possible, and else immediately return it to the outer function". ResultConstructor{E, Union{}}
are even more flexible in what they can be converted to: They can convert to any Option
type, or any Result{T, E2}
where E <: E2
. This allows you to propagate errors from functions returning Result
to those returning Option
.
Let's see it in action. Suppose you want to implement a safe version of the harmonic mean function, which in turn uses a safe version of div
:
safe_div(a::Integer, b::Real)::Option{Float64} = iszero(b) ? none : some(a/b)
function harmonic_mean(v::AbstractArray{<:Integer})::Option{Float64}
sm = 0.0
for i in v
invi = safe_div(1, i)
is_none(invi) && return none
sm += unwrap(invi)
end
res = safe_div(length(v), sm)
is_none(res) && return none
return some(unwrap(res))
end
In this function, we constantly have to check whether safe_div
returned the error value, and return that from the outer function in that case. That can be more concisely written as:
function harmonic_mean(v::AbstractArray{<:Integer})::Option{Float64}
sm = 0.0
for i in v
sm += @? safe_div(1, i)
end
some(@? safe_div(length(v), sm))
end
When to use an error type vs throw an error
The error handling mechanism provided by ErrorTypes is a distinct method from throwing and catching errors. None is superior to the other in all circumstances.
The handling provided by ErrorTypes is faster, safer, and more explicit. For most functions, you can use ErrorTypes. However, you can't only rely on it. Imagine a function A
returning Option{T1}
. A
is called from function B
, which can itself fail and returns an Option{T2}
. However, now there are two distinct error states: Failure in A
and failure in B
. So what should B
return? Result{T2, Enum{E1, E2}}
, for some Enum
type? But then, what about functions calling B
? Where does it end?
In general, it's un-idiomatic to "accumulate" error states like this. You should handle an error state when it appears, and usually not return it far back the call chain.
More importantly, you should distinguish between recoverable and unrecoverable error states. The unrecoverable are unexpected, and reveals that the program went wrong somehow. If the program went somewhere it shouldn't be, it's best to abort the program and show the stack trace, so you can debug it - here, an ordinary exception is better. If the errors are known to be possible beforehand, using ErrorTypes is better. For example, a program may use exceptions when encountering errors when parsing "internal" machine-generated files, which are supposed to be of a certain format, and use error types when parsing user input, which must always be expected to be possibly fallible.
Because error types are so easily converted to exceptions (using unwrap
and expect
), internal library functions should preferably use error types.
Reference
ErrorTypes.Err
— TypeErr
The error state of a Result{O, E}
, carrying an object of type E
. For convenience, Err(x)
creates a dummy value that can be converted to the appropriate Result type
.
ErrorTypes.Ok
— TypeOk{T}
The success state of a Result{T, E}
, carrying an object of type T
. For convenience, Ok(x)
creates a dummy value that can be converted to the appropriate Result type
.
ErrorTypes.Option
— TypeOption{T}
Alias for Result{T, Nothing}
ErrorTypes.Result
— TypeResult{O, E}
A sum type of either Ok{O}
or Err{E}
. Used as return value of functions that can error with an informative error object of type E
.
ErrorTypes.and_then
— Methodand_then(f, ::Type{T}, x::Result{O, E})
If is
a result value, apply f
to unwrap(x)
, else return the error value. Always returns a Result{T, E}
.
WARNING If f(unwrap(x))
is not a T
, this functions throws an error.
ErrorTypes.base
— Methodbase(x::Option{T})
Convert an Option{T}
to a Union{Some{T}, Nothing}
.
ErrorTypes.expect
— Methodexpect(x::Result, s::AbstractString)
If x
is of the associated error type, error with message s
. Else, return the contained result type.
ErrorTypes.expect_error
— Methodexpect_error(x::Result, s::AbstractString)
If x
contains an Err
, return the content of the Err
. Else, throw an error with message s
.
ErrorTypes.flatten
— Methodflatten(x::Option{Option{T}})
Convert an Option{Option{T}}
to an Option{T}
.
Examples
julia> flatten(some(some("x")))
some("x")
julia> flatten(some(none(Int)))
none(Int)
ErrorTypes.map_or
— Methodmap_or(f, x::Result, v)
If x
is a result value, return f(unwrap(x))
. Else, return v
.
ErrorTypes.unwrap
— Methodunwrap(x::Result)
If x
is of the associated error type, throw an error. Else, return the contained result type.
ErrorTypes.unwrap_error
— Methodunwrap_error(x::Result)
If x
contains an Err
, return the content of the Err
. Else, throw an error.
ErrorTypes.unwrap_error_or
— Methodunwrap_error_or(x::Result, v)
Like unwrap_or
, but unwraps an error.
ErrorTypes.unwrap_or
— Methodunwrap_or(x::Result, v)
If x
is an error value, return v
. Else, unwrap x
and return its content.
ErrorTypes.@?
— Macro@?(expr)
Propagate a Result
with Err
value to the outer function. Evaluate expr
, which should return a Result
. If it contains an Ok
value x
, evaluate to the unwrapped value x
. Else, evaluates to return Err(x)
.
Example
julia> (f(x::Option{T})::Option{T}) where T = Ok(@?(x) + one(T));
julia> f(some(1.0)), f(none(Int))
(some(2.0), none(Int64))
ErrorTypes.@unwrap_error_or
— Macro@unwrap_error_or(expr, exec)
Same as @unwrap_or
, but unwraps errors.
ErrorTypes.@unwrap_or
— Macro@unwrap_or(expr, exec)
Evaluate expr
to a Result
. If expr
is a error value, evaluate exec
and return that. Else, return the wrapped value in expr
.
Examples
julia> safe_inv(x)::Option{Float64} = iszero(x) ? none : Ok(1/x);
julia> function skip_inv_sum(it)
sum = 0.0
for i in it
sum += @unwrap_or safe_inv(i) continue
end
sum
end;
julia> skip_inv_sum([2,1,0,1,2])
3.0