Skip to content

Commit

Permalink
Make TryExperimental usable without as Try
Browse files Browse the repository at this point in the history
  • Loading branch information
tkf committed Mar 20, 2022
1 parent 76a4a06 commit b0b729a
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 146 deletions.
44 changes: 20 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ For more explanation, see [Discussion](#discussion) below.
### Basic usage

```julia
julia> import TryExperimental as Try
julia> using Try

julia> using TryExperimental # exports trygetindex etc.
```

Try.jl-based API return either an `OK` value

```julia
julia> ok = Try.getindex(Dict(:a => 111), :a)
julia> ok = trygetindex(Dict(:a => 111), :a)
Try.Ok: 111
```

or an `Err` value:

```julia
julia> err = Try.getindex(Dict(:a => 111), :b)
julia> err = trygetindex(Dict(:a => 111), :b)
Try.Err: KeyError: key :b not found
```

Expand Down Expand Up @@ -63,7 +65,7 @@ Consider an example where an error "bubbles up" from a deep stack of function
calls:

```JULIA
julia> import TryExperimental as Try
julia> using Try, TryExperimental

julia> f1(x) = x ? Ok(nothing) : Err(KeyError(:b));

Expand Down Expand Up @@ -122,18 +124,17 @@ been already started.
### EAFP

As explained in [EAFP and traits](#eafp-and-traits) below, the `Base`-like API
defined in `Try` namespace does not throw when the method is not defined. For
example, `Try.eltype` and `Try.length` can be called on arbitrary objects (=
defined in `TryExperimental` does not throw when the method is not defined. For
example, `trygeteltype` and `trygetlength` can be called on arbitrary objects (=
"asking for forgiveness") without checking if the method is defined (= "asking
for permission").

```julia
import TryExperimental as Try
using .Try
using Try, TryExperimental

function try_map_prealloc(f, xs)
T = Try.@return_err Try.eltype(xs) # macro-based short-circuiting
n = Try.@return_err Try.length(xs)
T = Try.@return_err trygeteltype(xs) # macro-based short-circuiting
n = Try.@return_err trygetlength(xs)
ys = Vector{T}(undef, n)
for (i, x) in zip(eachindex(ys), xs)
ys[i] = f(x)
Expand Down Expand Up @@ -174,20 +175,16 @@ return type of `Union{Ok,Err}`. Thus, the compiler can sometimes prove that
success or failure paths can never be taken:

```julia
julia> import TryExperimental as Try

julia> using .Try

julia> using InteractiveUtils
julia> using TryExperimental, InteractiveUtils

julia> @code_typed(Try.first((111, "two", :three)))[2] # always succeeds for non empty tuples
Try.Ok{Int64}
julia> @code_typed(trygetfirst((111, "two", :three)))[2] # always succeeds for non empty tuples
Ok{Int64}

julia> @code_typed(Try.first(()))[2] # always fails for an empty tuple
Try.Err{BoundsError}
julia> @code_typed(trygetfirst(()))[2] # always fails for an empty tuple
Err{BoundsError}

julia> @code_typed(Try.first(Int[]))[2] # both are possible for an array
Union{Try.Ok{Int64}, Try.Err{BoundsError}}
julia> @code_typed(trygetfirst(Int[]))[2] # both are possible for an array
Union{Ok{Int64}, Err{BoundsError}}
```

### Constraining returnable errors
Expand Down Expand Up @@ -218,8 +215,7 @@ Here is an example of providing the call API `tryparse` with the overload API
can return `InvalidCharError()` or `EndOfBufferError()` as an error value:

```julia
import TryExperimental as Try
using .Try
using Try, TryExperimental

struct InvalidCharError <: Exception end
struct EndOfBufferError <: Exception end
Expand Down Expand Up @@ -332,7 +328,7 @@ Importantly, the EAFP approach does not have the problem of the trait-based
feature detection where the implementer must ensure that declared trait (e.g.,
`HasLength`) is compatible with the actual definition (e.g., `length`). With
the EAFP approach, *the feature is declared automatically by defining of the
method providing it* (e.g., `Try.length`). Thus, by construction, it is hard to
method providing it* (e.g., `trygetlength`). Thus, by construction, it is hard to
make the feature declaration and definition out-of-sync. Of course, this
approach works only for effect-free or "redo-able" functions. To check if a
sequence of destructive operations is possible, the trait-based approach is
Expand Down
14 changes: 6 additions & 8 deletions examples/inferrability.jl
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
module UnionTyped
import TryExperimental
const Try = TryExperimental
using .Try
using Try
using TryExperimental
g(xs) = Ok(xs)
f(xs) = g(xs) |> Try.and_then(xs -> Try.getindex(xs, 1)) |> Try.ok
f(xs) = g(xs) |> Try.and_then(xs -> trygetindex(xs, 1)) |> Try.ok
end # module UnionTyped

module ConcretelyTyped
import TryExperimental
const Try = TryExperimental
using .Try
using Try
using TryExperimental
g(xs) = Try.ConcreteOk(xs)
function trygetfirst(xs)::Try.ConcreteResult{eltype(xs),BoundsError}
Try.getindex(xs, 1)
trygetindex(xs, 1)
end
f(xs) = g(xs) |> Try.and_then(trygetfirst) |> Try.ok
end # module
50 changes: 22 additions & 28 deletions lib/TryExperimental/src/TryExperimental.jl
Original file line number Diff line number Diff line change
@@ -1,47 +1,41 @@
baremodule TryExperimental

import Try

module InternalPrelude
include("prelude.jl")
end # module InternalPrelude
InternalPrelude.@reexport_try

Try.@function convert
# Try.@function promote
InternalPrelude.@exported_function tryconvert
# InternalPrelude.@exported_function trypromote

# Collection interface
Try.@function length
Try.@function eltype

Try.@function getindex
Try.@function setindex!

Try.@function first
Try.@function last
InternalPrelude.@exported_function trygetlength
InternalPrelude.@exported_function trygeteltype

Try.@function push!
Try.@function pushfirst!
Try.@function pop!
Try.@function popfirst!
InternalPrelude.@exported_function trygetindex
InternalPrelude.@exported_function trysetindex!

Try.@function put!
Try.@function take!
InternalPrelude.@exported_function trygetfirst
InternalPrelude.@exported_function trygetlast

Try.@function push_nowait!
Try.@function pushfirst_nowait!
Try.@function pop_nowait!
Try.@function popfirst_nowait!
InternalPrelude.@exported_function trypush!
InternalPrelude.@exported_function trypushfirst!
InternalPrelude.@exported_function trypop!
InternalPrelude.@exported_function trypopfirst!

Try.@function put_nowait!
Try.@function take_nowait!
InternalPrelude.@exported_function tryput!
InternalPrelude.@exported_function trytake!

module Internal

import ..TryExperimental
const Try = TryExperimental
using .Try
using .Try: Causes
using Try
using Try: Causes

for n in names(TryExperimental; all = true)
startswith(string(n), "try") || continue
fn = getproperty(TryExperimental, n)
@eval import TryExperimental: $n
end

using Base: IteratorEltype, HasEltype, IteratorSize, HasLength, HasShape

Expand Down
61 changes: 30 additions & 31 deletions lib/TryExperimental/src/base.jl
Original file line number Diff line number Diff line change
@@ -1,92 +1,91 @@
Try.convert(::Type{T}, x::T) where {T} = Ok(x) # TODO: should it be `Ok{T}(x)`?
tryconvert(::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 =
trygetlength(xs::MightHaveSize)::Result =
if IteratorSize(xs) isa Union{HasLength,HasShape}
return Ok(length(xs))
else
return Causes.notimplemented(Try.length, (xs,))
return Causes.notimplemented(trygetlength, (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)
trygeteltype(xs) = trygeteltype(typeof(xs))
trygeteltype(T::Type) = Causes.notimplemented(trygeteltype, (T,))
trygeteltype(::Type{Union{}}) = Causes.notimplemented(trygeteltype, (Union{},))
trygeteltype(::Type{<:AbstractArray{T}}) where {T} = Ok(T)
trygeteltype(::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)
trygeteltype(::Type{Dict}) where {K,V,Dict<:AbstractDict{K,V}} = eltype_impl(Dict)
trygeteltype(::Type{Num}) where {Num<:Number} = eltype_impl(Num)
trygeteltype(::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,))
return Causes.notimplemented(trygeteltype, (T,))
end

@inline function Try.getindex(a::AbstractArray, i...)::Result
@inline function trygetindex(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
@inline function trysetindex!(a::AbstractArray, v, i...)::Result
(@boundscheck checkbounds(Bool, a, i...)) || return Err(BoundsError(a, i))
@inbounds a[i...] = v
return Ok(v)
end

@inline function Try.getindex(xs::Tuple, i::Integer):: Result
@inline function trygetindex(xs::Tuple, i::Integer)::Result
i < 1 && return Err(BoundsError(xs, i))
i > length(xs) && return Err(BoundsError(xs, i))
return Ok(xs[i])
end

struct NotFound end

function Try.getindex(dict::AbstractDict, key)::Result
function trygetindex(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
function trysetindex!(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...))
trygetindex(dict::AbstractDict, k1, k2, ks...) = trygetindex(dict, (k1, k2, ks...))
trysetindex!(dict::AbstractDict, v, k1, k2, ks...) = trysetindex!(dict, v, (k1, k2, ks...))

Try.first(xs) = Try.getindex(xs, 1)
Try.last(xs) = Try.getindex(xs, lastindex(xs))
trygetfirst(xs) = trygetindex(xs, 1)
trygetlast(xs) = trygetindex(xs, lastindex(xs))

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))
trypop!(a::Vector)::Result = isempty(a) ? Causes.empty(a) : Ok(pop!(a))
trypopfirst!(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)
function trypush!(a::Vector, x)::Result
y = Try.@return_err tryconvert(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)
function trypushfirst!(a::Vector, x)::Result
y = Try.@return_err tryconvert(eltype(a), x)
pushfirst!(a, y)
return Ok(a)
end

function Try.take!(ch::Channel)::Result
function trytake!(ch::Channel)::Result
y = iterate(ch)
y === nothing && return Causes.empty(ch)
return Ok(first(y))
end

function Try.put!(ch::Channel, x)::Result
function tryput!(ch::Channel, x)::Result
isopen(ch) || return Causes.closed(ch)
y = Try.@return_err Try.convert(eltype(ch), x)
y = Try.@return_err tryconvert(eltype(ch), x)
try
put!(ch, x)
catch err
Expand Down
23 changes: 5 additions & 18 deletions lib/TryExperimental/src/prelude.jl
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
using Try

macro reexport_try()
exprs = []

mapfoldl(append!, names(Try; all = true); init = exprs) do name
value = try
getproperty(Try, name)
catch err
@error "Cannot access `Try.$name`" exception = (err, catch_backtrace())
return []
end
(value isa Module && value !== Try.Causes) && return []
return [:(const $name = $Try.$name)]
end

public_names = filter(!=(:Try), names(Try))
export_expr = :(export $(public_names...))

return esc(Expr(:block, __source__, exprs..., export_expr))
macro exported_function(name::Symbol)
quote
$Try.@function($name)
export $name
end |> esc
end
Loading

0 comments on commit b0b729a

Please sign in to comment.