Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
tkf committed Jan 6, 2022
1 parent e83957c commit 7054034
Show file tree
Hide file tree
Showing 28 changed files with 819 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
/Manifest.toml
Manifest.toml
8 changes: 1 addition & 7 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,4 @@ authors = ["Takafumi Arakaki <aka.tkf@gmail.com> and contributors"]
version = "0.1.0-DEV"

[compat]
julia = "1"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
julia = "1.6"
102 changes: 101 additions & 1 deletion README.md
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`.
2 changes: 2 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
site/
2 changes: 2 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
31 changes: 31 additions & 0 deletions docs/make.jl
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>"
)=#
9 changes: 9 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Try.jl

```@docs
Try
Result
Ok
Err
```

130 changes: 128 additions & 2 deletions src/Try.jl
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
88 changes: 88 additions & 0 deletions src/base.jl
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
Loading

0 comments on commit 7054034

Please sign in to comment.