-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
28 changed files
with
819 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
/Manifest.toml | ||
Manifest.toml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,101 @@ | ||
# Try | ||
# Try.jl: zero-overhead and debuggable error handling | ||
|
||
Features: | ||
|
||
* Error handling as simple manipulations of *values*. | ||
* Focus on *inferrability* and *optimizability* leveraging unique properties of | ||
the Julia language and compiler. | ||
* *Error trace* for determining the source of errors, without `throw`. | ||
* Facilitate the ["Easier to ask for forgiveness than permission" | ||
(EAFP)](https://docs.python.org/3/glossary.html#term-EAFP) approach as a | ||
robust and minimalistic alternative to the trait-based feature detection. | ||
|
||
## Examples | ||
|
||
### Basic usage | ||
|
||
```julia | ||
julia> using Try | ||
|
||
julia> result = Try.getindex(Dict(:a => 111), :a); | ||
|
||
julia> Try.isok(result) | ||
true | ||
|
||
julia> Try.unwrap(result) | ||
111 | ||
|
||
julia> result = Try.getindex(Dict(:a => 111), :b); | ||
|
||
julia> Try.iserr(result) | ||
true | ||
|
||
julia> Try.unwrap_err(result) | ||
KeyError(:b) | ||
``` | ||
|
||
### EAFP | ||
|
||
```julia | ||
using Try | ||
|
||
function try_map_prealloc(f, xs) | ||
T = Try.@return_err Try.eltype(xs) | ||
n = Try.@return_err Try.length(xs) | ||
ys = Vector{T}(undef, n) | ||
for (i, x) in zip(eachindex(ys), xs) | ||
ys[i] = f(x) | ||
end | ||
return Ok(ys) | ||
end | ||
|
||
mymap(f, xs) = | ||
try_map_prealloc(f, xs) |> | ||
Try.or_else() do _ | ||
Ok(mapfoldl(f, push!, xs; init = [])) | ||
end |> | ||
Try.unwrap | ||
|
||
mymap(x -> x + 1, 1:3) | ||
|
||
# output | ||
3-element Vector{Int64}: | ||
2 | ||
3 | ||
4 | ||
``` | ||
|
||
```julia | ||
mymap(x -> x + 1, (x for x in 1:5 if isodd(x))) | ||
|
||
# output | ||
3-element Vector{Any}: | ||
2 | ||
4 | ||
6 | ||
``` | ||
|
||
## Discussion | ||
|
||
Try.jl provides an API inspired by Rust's `Result` type. However, to fully | ||
unlock the power of Julia, Try.jl uses the *small `Union` types* instead of a | ||
concretely typed sum type. Furthermore, it optionally supports concretely-typed | ||
returned value when `Union` is not appropriate. | ||
|
||
A potential usability issue for using the `Result` type is that the detailed | ||
context of the error is lost by the time the user received an error. This makes | ||
debugging Julia programs hard compared to simply `throw`ing the exception. To | ||
solve this problem, Try.jl provides an *error trace* mechanism for recording the | ||
backtrace of the error. This can be toggled using `Try.enable_errortrace()` at | ||
the run-time. This is inspired by Zig's [Error Return | ||
Traces](https://ziglang.org/documentation/master/#Error-Return-Traces). | ||
|
||
Try.jl exposes a limited set of "verbs" based on Julia `Base` such as | ||
`Try.take!`. These functions have a catch-all default definition that returns | ||
an error value `Err{NotImplementedError}`. This let us use these functions in | ||
the ["Easier to ask for forgiveness than permission" | ||
(EAFP)](https://docs.python.org/3/glossary.html#term-EAFP) manner because they | ||
can be called without getting the run-time `MethodError` exception. Such | ||
functions can be defined using `Try.@function f` instead of `function f end`. | ||
They are defined as instances of `Tryable <: Function` and not as a direct | ||
instance of `Function`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
build/ | ||
site/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[deps] | ||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
using Documenter | ||
using Try | ||
|
||
makedocs( | ||
sitename = "Try", | ||
format = Documenter.HTML(), | ||
modules = [Try], | ||
strict = [ | ||
:autodocs_block, | ||
:cross_references, | ||
:docs_block, | ||
:doctest, | ||
:eval_block, | ||
:example_block, | ||
:footnote, | ||
:linkcheck, | ||
:meta_block, | ||
# :missing_docs, | ||
:parse_error, | ||
:setup_block, | ||
], | ||
# Ref: | ||
# https://juliadocs.github.io/Documenter.jl/stable/lib/public/#Documenter.makedocs | ||
) | ||
|
||
# Documenter can also automatically deploy documentation to gh-pages. | ||
# See "Hosting Documentation" and deploydocs() in the Documenter manual | ||
# for more information. | ||
#=deploydocs( | ||
repo = "<repository url>" | ||
)=# |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Try.jl | ||
|
||
```@docs | ||
Try | ||
Result | ||
Ok | ||
Err | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,137 @@ | ||
baremodule Try | ||
|
||
export Ok, Err, Result | ||
|
||
using Base: Base, Exception | ||
|
||
abstract type AbstractResult{T,E<:Exception} end | ||
|
||
struct Ok{T} <: AbstractResult{T,Union{}} | ||
value::T | ||
end | ||
|
||
struct Err{E<:Exception} <: AbstractResult{Union{},E} | ||
value::E | ||
backtrace::Union{Nothing,typeof(Base.backtrace())} | ||
end | ||
|
||
const DynamicResult{T,E} = Union{Ok{T},Err{E}} | ||
|
||
struct ConcreteResult{T,E<:Exception} <: AbstractResult{T,E} | ||
value::DynamicResult{T,E} | ||
ConcreteResult{T,E}(value::DynamicResult{T,E}) where {T,E<:Exception} = new{T,E}(value) | ||
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}} | ||
|
||
function throw end | ||
|
||
function unwrap end | ||
function unwrap_err end | ||
|
||
function ok end | ||
function err end | ||
function oktype end | ||
function errtype end | ||
function isok end | ||
function iserr end | ||
|
||
function enable_errortrace end | ||
function disable_errortrace end | ||
|
||
abstract type Tryable <: Function end | ||
|
||
# Core exceptions | ||
struct IsOkError <: Exception | ||
ok::AbstractResult{<:Any,Union{}} | ||
end | ||
|
||
# Basic exceptions | ||
abstract type NotImplementedError <: Exception end | ||
abstract type ClosedError <: Exception end | ||
abstract type EmptyError <: Exception end | ||
abstract type FullError <: Exception end | ||
|
||
baremodule Causes | ||
function notimplemented end | ||
function empty end | ||
function closed end | ||
end # baremodule Cause | ||
|
||
macro and_then end | ||
macro or_else end | ||
macro return_err end | ||
function var"@return" end | ||
function var"@function" end | ||
|
||
function and_then end | ||
function or_else end | ||
|
||
module Internal | ||
|
||
using ..Try: Try | ||
import ..Try: @return, @return_err, @and_then, @or_else, @function | ||
using ..Try: | ||
AbstractResult, | ||
Causes, | ||
ConcreteErr, | ||
ConcreteOk, | ||
ConcreteResult, | ||
DynamicResult, | ||
Err, | ||
Ok, | ||
Result, | ||
Try | ||
|
||
include("internal.jl") | ||
using Base.Meta: isexpr | ||
|
||
include("utils.jl") | ||
include("core.jl") | ||
include("show.jl") | ||
include("errortrace.jl") | ||
include("function.jl") | ||
include("causes.jl") | ||
|
||
include("tools.jl") | ||
include("sugar.jl") | ||
|
||
end # module Internal | ||
|
||
@function convert | ||
# @function promote | ||
|
||
# Collection interface | ||
@function length | ||
@function eltype | ||
|
||
@function getindex | ||
@function setindex! | ||
|
||
@function push! | ||
@function pushfirst! | ||
@function pop! | ||
@function popfirst! | ||
|
||
@function put! | ||
@function take! | ||
|
||
@function push_nowait! | ||
@function pushfirst_nowait! | ||
@function pop_nowait! | ||
@function popfirst_nowait! | ||
|
||
@function put_nowait! | ||
@function take_nowait! | ||
|
||
module Implementations | ||
using ..Try | ||
using ..Try: Causes | ||
using Base: IteratorEltype, HasEltype, IteratorSize, HasLength, HasShape | ||
include("base.jl") | ||
end # module Implementations | ||
|
||
Internal.define_docstrings() | ||
|
||
end # baremodule Try |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
Try.convert(::Type{T}, x::T) where {T} = Ok(x) # TODO: should it be `Ok{T}(x)`? | ||
|
||
const MightHaveSize = Union{AbstractArray,AbstractDict,AbstractSet,AbstractString,Number} | ||
|
||
Try.length(xs::MightHaveSize)::Result = | ||
if IteratorSize(xs) isa Union{HasLength,HasShape} | ||
return Ok(length(xs)) | ||
else | ||
return Causes.notimplemented(Try.length, (xs,)) | ||
end | ||
|
||
Try.eltype(xs) = Try.eltype(typeof(xs)) | ||
Try.eltype(T::Type) = Causes.notimplemented(Try.eltype, (T,)) | ||
Try.eltype(::Type{Union{}}) = Causes.notimplemented(Try.eltype, (Union{},)) | ||
Try.eltype(::Type{<:AbstractArray{T}}) where {T} = Ok(T) | ||
Try.eltype(::Type{AbstractSet{T}}) where {T} = Ok(T) | ||
|
||
Try.eltype(::Type{Dict}) where {K,V,Dict<:AbstractDict{K,V}} = eltype_impl(Dict) | ||
Try.eltype(::Type{Num}) where {Num<:Number} = eltype_impl(Num) | ||
Try.eltype(::Type{Str}) where {Str<:AbstractString} = eltype_impl(Str) | ||
|
||
eltype_impl(::Type{T}) where {T} = | ||
if IteratorEltype(T) isa HasEltype | ||
return Ok(eltype(T)) | ||
else | ||
return Causes.notimplemented(Try.eltype, (T,)) | ||
end | ||
|
||
@inline function Try.getindex(a::AbstractArray, i...)::Result | ||
(@boundscheck checkbounds(Bool, a, i...)) || return Err(BoundsError(a, i)) | ||
return Ok(@inbounds a[i...]) | ||
end | ||
|
||
@inline function Try.setindex!(a::AbstractArray, v, i...)::Result | ||
(@boundscheck checkbounds(Bool, a, i...)) || return Err(BoundsError(a, i)) | ||
@inbounds a[i...] = v | ||
return Ok(v) | ||
end | ||
|
||
struct NotFound end | ||
|
||
function Try.getindex(dict::AbstractDict, key)::Result | ||
value = get(dict, key, NotFound()) | ||
value isa NotFound && return Err(KeyError(key)) | ||
return Ok(value) | ||
end | ||
|
||
function Try.setindex!(dict::AbstractDict, value, key)::Result | ||
dict[key] = value | ||
return Ok(value) | ||
end | ||
|
||
Try.getindex(dict::AbstractDict, k1, k2, ks...) = Try.getindex(dict, (k1, k2, ks...)) | ||
Try.setindex!(dict::AbstractDict, v, k1, k2, ks...) = | ||
Try.setindex!(dict, v, (k1, k2, ks...)) | ||
|
||
Try.pop!(a::Vector)::Result = isempty(a) ? Causes.empty(a) : Ok(pop!(a)) | ||
Try.popfirst!(a::Vector)::Result = isempty(a) ? Causes.empty(a) : Ok(popfirst!(a)) | ||
|
||
function Try.push!(a::Vector, x)::Result | ||
y = Try.@return_err Try.convert(eltype(a), x) | ||
push!(a, y) | ||
return Ok(a) | ||
end | ||
|
||
function Try.pushfirst!(a::Vector, x)::Result | ||
y = Try.@return_err Try.convert(eltype(a), x) | ||
pushfirst!(a, y) | ||
return Ok(a) | ||
end | ||
|
||
function Try.take!(ch::Channel)::Result | ||
y = iterate(ch) | ||
y === nothing && return Causes.empty(ch) | ||
return Ok(first(y)) | ||
end | ||
|
||
function Try.put!(ch::Channel, x)::Result | ||
isopen(ch) || return Causes.closed(ch) | ||
y = Try.@return_err Try.convert(eltype(ch), x) | ||
try | ||
put!(ch, x) | ||
catch err | ||
err isa InvalidStateException && !isopen(ch) && return Causes.closed(ch) | ||
rethrow() | ||
end | ||
return Ok(y) | ||
end |
Oops, something went wrong.