diff --git a/README.md b/README.md index 6c478c2..d49818a 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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)); @@ -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) @@ -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 @@ -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 @@ -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 diff --git a/examples/inferrability.jl b/examples/inferrability.jl index 3e3f6c1..bcc8b9b 100644 --- a/examples/inferrability.jl +++ b/examples/inferrability.jl @@ -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 diff --git a/lib/TryExperimental/src/TryExperimental.jl b/lib/TryExperimental/src/TryExperimental.jl index 7506b6b..9a58e17 100644 --- a/lib/TryExperimental/src/TryExperimental.jl +++ b/lib/TryExperimental/src/TryExperimental.jl @@ -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 diff --git a/lib/TryExperimental/src/base.jl b/lib/TryExperimental/src/base.jl index d52e2eb..4aa1cbe 100644 --- a/lib/TryExperimental/src/base.jl +++ b/lib/TryExperimental/src/base.jl @@ -1,43 +1,43 @@ -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]) @@ -45,48 +45,47 @@ 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 diff --git a/lib/TryExperimental/src/prelude.jl b/lib/TryExperimental/src/prelude.jl index e43d905..e59329f 100644 --- a/lib/TryExperimental/src/prelude.jl +++ b/lib/TryExperimental/src/prelude.jl @@ -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 diff --git a/test/TryTests/src/test_base.jl b/test/TryTests/src/test_base.jl index d06fcee..c778052 100644 --- a/test/TryTests/src/test_base.jl +++ b/test/TryTests/src/test_base.jl @@ -1,55 +1,53 @@ module TestBase using Test - -import TryExperimental -const Try = TryExperimental -using .Try +using Try +using TryExperimental function test_convert() - @test Try.unwrap(Try.convert(Int, 1)) === 1 - @test Try.unwrap(Try.convert(Union{Int,String}, 1)) === 1 - @test Try.iserr(Try.convert(Union{}, 1)) - @test Try.iserr(Try.convert(String, 1)) - @test Try.unwrap_err(Try.convert(Nothing, 1)) isa Try.NotImplementedError + @test Try.unwrap(tryconvert(Int, 1)) === 1 + @test Try.unwrap(tryconvert(Union{Int,String}, 1)) === 1 + @test Try.iserr(tryconvert(Union{}, 1)) + @test Try.iserr(tryconvert(String, 1)) + @test Try.unwrap_err(tryconvert(Nothing, 1)) isa Try.NotImplementedError end function test_length() - @test Try.unwrap_err(Try.length(nothing)) isa Try.NotImplementedError - @test Try.unwrap_err(Try.length(x for x in 1:10 if isodd(x))) isa + @test Try.unwrap_err(trygetlength(nothing)) isa Try.NotImplementedError + @test Try.unwrap_err(trygetlength(x for x in 1:10 if isodd(x))) isa Try.NotImplementedError - @test Try.unwrap(Try.length([1])) == 1 + @test Try.unwrap(trygetlength([1])) == 1 end function test_eltype() - @test Try.unwrap(Try.eltype(1)) === Int - @test Try.unwrap(Try.eltype([1])) === Int - @test Try.unwrap(Try.eltype(AbstractVector{Int})) === Int - @test Try.unwrap(Try.eltype(AbstractArray{Int})) === Int - @test Try.unwrap_err(Try.eltype(AbstractVector)) isa Try.NotImplementedError + @test Try.unwrap(trygeteltype(1)) === Int + @test Try.unwrap(trygeteltype([1])) === Int + @test Try.unwrap(trygeteltype(AbstractVector{Int})) === Int + @test Try.unwrap(trygeteltype(AbstractArray{Int})) === Int + @test Try.unwrap_err(trygeteltype(AbstractVector)) isa Try.NotImplementedError end function test_getindex() - @test Try.unwrap(Try.getindex([111], 1)) === 111 - @test Try.unwrap_err(Try.getindex([111], 0)) isa BoundsError - @test Try.unwrap_err(Try.getindex([111], 2)) isa BoundsError + @test Try.unwrap(trygetindex([111], 1)) === 111 + @test Try.unwrap_err(trygetindex([111], 0)) isa BoundsError + @test Try.unwrap_err(trygetindex([111], 2)) isa BoundsError - @test Try.unwrap(Try.getindex(Dict(:a => 111), :a)) === 111 - @test Try.unwrap_err(Try.getindex(Dict(:a => 111), :b)) isa KeyError + @test Try.unwrap(trygetindex(Dict(:a => 111), :a)) === 111 + @test Try.unwrap_err(trygetindex(Dict(:a => 111), :b)) isa KeyError end function test_first() - @test Try.unwrap(Try.first([111, 222, 333])) === 111 - @test Try.unwrap(Try.first((111, 222, 333))) === 111 - @test Try.iserr(Try.first([])) - @test Try.iserr(Try.first(())) + @test Try.unwrap(trygetfirst([111, 222, 333])) === 111 + @test Try.unwrap(trygetfirst((111, 222, 333))) === 111 + @test Try.iserr(trygetfirst([])) + @test Try.iserr(trygetfirst(())) end function test_last() - @test Try.unwrap(Try.last([111, 222, 333])) === 333 - @test Try.unwrap(Try.last((111, 222, 333))) === 333 - @test Try.iserr(Try.last([])) - @test Try.iserr(Try.last(())) + @test Try.unwrap(trygetlast([111, 222, 333])) === 333 + @test Try.unwrap(trygetlast((111, 222, 333))) === 333 + @test Try.iserr(trygetlast([])) + @test Try.iserr(trygetlast(())) end end # module diff --git a/test/TryTests/src/test_tools.jl b/test/TryTests/src/test_tools.jl index 14555b3..46ee93b 100644 --- a/test/TryTests/src/test_tools.jl +++ b/test/TryTests/src/test_tools.jl @@ -1,14 +1,12 @@ module TestTools using Test - -import TryExperimental -const Try = TryExperimental -using .Try +using Try +using TryExperimental function test_curry() value = - Try.convert(String, 1) |> + tryconvert(String, 1) |> Try.or_else() do _ Ok("123") end |> @@ -25,10 +23,10 @@ function demo_macro(xs) y = nothing while true #! format: off - x = @Try.or_else(Try.getindex(xs, i)) do _ + x = @Try.or_else(trygetindex(xs, i)) do _ return :oob end - @Try.and_then(Try.getindex(xs, Try.unwrap(x))) do z + @Try.and_then(trygetindex(xs, Try.unwrap(x))) do z if z > 1 y = z break