From 85ab1ec70d6a73aaa9e2a7ecb89f92898e8e361a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 20 Mar 2022 10:57:06 -0400 Subject: [PATCH] Generalize short-circuit APIs --- README.md | 4 +- docs/make.jl | 16 ++-- docs/src/experimental.md | 15 ++++ docs/src/index.md | 10 +++ lib/TryExperimental/src/TryExperimental.jl | 10 +++ lib/TryExperimental/src/base.jl | 6 +- lib/TryExperimental/src/docs/Break.md | 1 + lib/TryExperimental/src/docs/Continue.md | 1 + lib/TryExperimental/src/docs/branch.md | 5 ++ lib/TryExperimental/src/docs/resultof.md | 5 ++ lib/TryExperimental/src/docs/valueof.md | 5 ++ src/Try.jl | 14 ++-- src/branch.jl | 97 ++++++++++++++++++++++ src/docs/@?.md | 41 +++++++++ src/docs/@and_return.md | 48 +++++++++++ src/docs/and_then.md | 41 +++++++++ src/docs/or_else.md | 47 +++++++++++ src/sugar.jl | 24 ------ src/tools.jl | 33 -------- test/TryTests/src/test_doctest.jl | 8 +- test/TryTests/src/test_tools.jl | 56 +++++++++++++ 21 files changed, 410 insertions(+), 77 deletions(-) create mode 100644 docs/src/experimental.md create mode 100644 lib/TryExperimental/src/docs/Break.md create mode 100644 lib/TryExperimental/src/docs/Continue.md create mode 100644 lib/TryExperimental/src/docs/branch.md create mode 100644 lib/TryExperimental/src/docs/resultof.md create mode 100644 lib/TryExperimental/src/docs/valueof.md create mode 100644 src/branch.jl create mode 100644 src/docs/@?.md create mode 100644 src/docs/@and_return.md create mode 100644 src/docs/and_then.md create mode 100644 src/docs/or_else.md delete mode 100644 src/tools.jl diff --git a/README.md b/README.md index d49818a..e3463da 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,8 @@ for permission"). using Try, TryExperimental function try_map_prealloc(f, xs) - T = Try.@return_err trygeteltype(xs) # macro-based short-circuiting - n = Try.@return_err trygetlength(xs) + T = @? trygeteltype(xs) # macro-based short-circuiting + n = @? trygetlength(xs) ys = Vector{T}(undef, n) for (i, x) in zip(eachindex(ys), xs) ys[i] = f(x) diff --git a/docs/make.jl b/docs/make.jl index 39ea40c..930474f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,10 +1,11 @@ using Documenter using Try +using TryExperimental makedocs( sitename = "Try", format = Documenter.HTML(), - modules = [Try], + modules = [Try, TryExperimental], strict = [ :autodocs_block, :cross_references, @@ -23,9 +24,10 @@ makedocs( # 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 = "" -)=# +deploydocs( + repo = "github.com/tkf/Try.jl", + devbranch = "main", + push_preview = true, + # Ref: + # https://juliadocs.github.io/Documenter.jl/stable/lib/public/#Documenter.deploydocs +) diff --git a/docs/src/experimental.md b/docs/src/experimental.md new file mode 100644 index 0000000..e51de43 --- /dev/null +++ b/docs/src/experimental.md @@ -0,0 +1,15 @@ +# Experimental + +```@meta +CurrentModule = Main +``` + +## [Customizing short-circuit evaluation](@id customize-short-circuit) + +```@docs +TryExperimental.branch +TryExperimental.Break +TryExperimental.Continue +TryExperimental.resultof +TryExperimental.valueof +``` diff --git a/docs/src/index.md b/docs/src/index.md index b8a8140..7f2269c 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -7,3 +7,13 @@ Ok Err ``` +## Short-circuit evaluation + +```@docs +@? +Try.@and_return +Try.or_else +Try.and_then +``` + +See also: [Customizing short-circuit evaluation](@ref customize-short-circuit). diff --git a/lib/TryExperimental/src/TryExperimental.jl b/lib/TryExperimental/src/TryExperimental.jl index bcef755..1709108 100644 --- a/lib/TryExperimental/src/TryExperimental.jl +++ b/lib/TryExperimental/src/TryExperimental.jl @@ -1,5 +1,7 @@ baremodule TryExperimental +import Try + module InternalPrelude include("prelude.jl") end # module InternalPrelude @@ -25,6 +27,12 @@ InternalPrelude.@exported_function trypopfirst! InternalPrelude.@exported_function tryput! InternalPrelude.@exported_function trytake! +const Break = Try.Internal.Break +const Continue = Try.Internal.Continue +const branch = Try.Internal.branch +const resultof = Try.Internal.resultof +const valueof = Try.Internal.valueof + # Basic exceptions abstract type EmptyError <: Exception end abstract type ClosedError <: Exception end @@ -64,4 +72,6 @@ include("maybe.jl") end end # module Maybe +Internal.Try.Internal.@define_docstrings + end # baremodule TryExperimental diff --git a/lib/TryExperimental/src/base.jl b/lib/TryExperimental/src/base.jl index 4aa1cbe..81e7373 100644 --- a/lib/TryExperimental/src/base.jl +++ b/lib/TryExperimental/src/base.jl @@ -66,13 +66,13 @@ 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 trypush!(a::Vector, x)::Result - y = Try.@return_err tryconvert(eltype(a), x) + y = @? tryconvert(eltype(a), x) push!(a, y) return Ok(a) end function trypushfirst!(a::Vector, x)::Result - y = Try.@return_err tryconvert(eltype(a), x) + y = @? tryconvert(eltype(a), x) pushfirst!(a, y) return Ok(a) end @@ -85,7 +85,7 @@ end function tryput!(ch::Channel, x)::Result isopen(ch) || return Causes.closed(ch) - y = Try.@return_err tryconvert(eltype(ch), x) + y = @? tryconvert(eltype(ch), x) try put!(ch, x) catch err diff --git a/lib/TryExperimental/src/docs/Break.md b/lib/TryExperimental/src/docs/Break.md new file mode 100644 index 0000000..fd0c91d --- /dev/null +++ b/lib/TryExperimental/src/docs/Break.md @@ -0,0 +1 @@ + TryExperimental.Break(result) diff --git a/lib/TryExperimental/src/docs/Continue.md b/lib/TryExperimental/src/docs/Continue.md new file mode 100644 index 0000000..00794f1 --- /dev/null +++ b/lib/TryExperimental/src/docs/Continue.md @@ -0,0 +1 @@ + TryExperimental.Continue(result) diff --git a/lib/TryExperimental/src/docs/branch.md b/lib/TryExperimental/src/docs/branch.md new file mode 100644 index 0000000..17cb414 --- /dev/null +++ b/lib/TryExperimental/src/docs/branch.md @@ -0,0 +1,5 @@ + TryExperiment.branch(result) -> Continue(result) + TryExperiment.branch(result) -> Break(result) + +`branch` implements a short-circuiting evaluation API. It must return a `Continue` or a +`Break`. diff --git a/lib/TryExperimental/src/docs/resultof.md b/lib/TryExperimental/src/docs/resultof.md new file mode 100644 index 0000000..c139682 --- /dev/null +++ b/lib/TryExperimental/src/docs/resultof.md @@ -0,0 +1,5 @@ + TryExperimental.resultof(branch) -> result + TryExperimental.resultof(branch::Continue{<:Ok}) -> result::Ok + TryExperimental.resultof(branch::Break{<:Err}) -> result::Err + TryExperimental.resultof(branch::Continue{<:Some}) -> result::Some + TryExperimental.resultof(branch::Break{Nothing}) -> nothing diff --git a/lib/TryExperimental/src/docs/valueof.md b/lib/TryExperimental/src/docs/valueof.md new file mode 100644 index 0000000..207ad40 --- /dev/null +++ b/lib/TryExperimental/src/docs/valueof.md @@ -0,0 +1,5 @@ + TryExperimental.valueof(branch) -> value + TryExperimental.valueof(branch::Continue{Ok{T}}) -> value::T + TryExperimental.valueof(branch::Break{Err{T}}) -> value::T + TryExperimental.valueof(branch::Continue{Some{T}}) -> value::T + TryExperimental.valueof(branch::Break{Nothing}) -> nothing diff --git a/src/Try.jl b/src/Try.jl index 121314e..c18198d 100644 --- a/src/Try.jl +++ b/src/Try.jl @@ -1,6 +1,6 @@ baremodule Try -export Ok, Err, Result +export @?, Ok, Err, Result using Base: Base, Exception @@ -46,6 +46,7 @@ function enable_errortrace end function disable_errortrace end function istryable end +function var"@function" end # Core exceptions struct IsOkError <: Exception @@ -58,16 +59,16 @@ abstract type NotImplementedError <: Exception end macro and_then end macro or_else end -macro return_err end -function var"@return" end -function var"@function" end + +macro and_return end +function var"@?" end function and_then end function or_else end module Internal -import ..Try: @return, @return_err, @and_then, @or_else, @function +import ..Try: @and_return, @?, @and_then, @or_else, @function using ..Try: AbstractResult, ConcreteErr, @@ -90,7 +91,8 @@ include("show.jl") include("errortrace.jl") include("function.jl") -include("tools.jl") +include("branch.jl") + include("sugar.jl") end # module Internal diff --git a/src/branch.jl b/src/branch.jl new file mode 100644 index 0000000..989624f --- /dev/null +++ b/src/branch.jl @@ -0,0 +1,97 @@ +### +### Experimental API +### + +# This is very similar to Rust's `Try`` trait and `ControlFlow` enum. +# https://doc.rust-lang.org/std/ops/trait.Try.html +# https://doc.rust-lang.org/std/result/enum.Result.html +# https://doc.rust-lang.org/std/ops/enum.ControlFlow.html + +struct Break{T} + result::T +end + +struct Continue{T} + result::T +end + +function branch end +function resultof end +function valueof end + +### +### Implementation +### + +branch(ok::Ok) = Continue(ok) +branch(err::Err) = Break(err) +branch(result::AbstractResult) = + if Try.isok(result) + Continue(result) + else + Break(result) + end + +resultof(br) = br.result + +valueof(br::Continue{<:AbstractResult}) = Try.unwrap(br.result) +valueof(br::Break{<:AbstractResult}) = Try.unwrap_err(br.result) + +branch(some::Some) = Continue(some) +branch(::Nothing) = Break(nothing) + +valueof(br::Continue{<:Some}) = something(br.result) +valueof(::Break{Nothing}) = nothing + +const var"@or_return" = var"@?" +macro or_return(ex) # aka @? + quote + br = branch($(esc(ex))) + if br isa Break + return br.result + else + valueof(br) + end + end +end + +macro and_return(ex) + quote + br = branch($(esc(ex))) + if br isa Continue + return br.result + else + valueof(br) + end + end +end + +function Try.and_then(f::F) where {F} + function and_then_closure(result) + Try.and_then(f, result) + end +end + +function Try.and_then(f, result) + br = branch(result) + if br isa Continue + f(valueof(br)) + else + br.result + end +end + +function Try.or_else(f::F) where {F} + function or_else_closure(result) + Try.or_else(f, result) + end +end + +function Try.or_else(f, result) + br = branch(result) + if br isa Break + f(valueof(br)) + else + br.result + end +end diff --git a/src/docs/@?.md b/src/docs/@?.md new file mode 100644 index 0000000..98e4188 --- /dev/null +++ b/src/docs/@?.md @@ -0,0 +1,41 @@ + @? result + +Evaluates to an unwrapped "success" result value; return `result` if it is a "failure." + +If `result` is an `Ok` or a `Some`, `@?` is equivalent to unwrapping the value. If `result` +is an `Err` or `nothing`, `@?` is equivalent to `return`. + +| Invocation | Equivalent code | +|:--- |:--- | +| `@? Ok(value)` | `value` | +| `@? Err(value)` | `return value` | +| `@? Some(value)` | `value` | +| `@? nothing` | `return nothing` | + +See also: [`@and_return`](@ref), [`and_then`](@ref), [`or_else`](@ref). + +# Extended help + +## Examples + +```julia +using Try, TryExperimental + +function try_map_prealloc(f, xs) + T = @? trygeteltype(xs) # macro-based short-circuiting + n = @? trygetlength(xs) + ys = Vector{T}(undef, n) + for (i, x) in zip(eachindex(ys), xs) + ys[i] = f(x) + end + return Ok(ys) +end + +Try.unwrap(try_map_prealloc(x -> x + 1, 1:3)) + +# output +3-element Vector{Int64}: + 2 + 3 + 4 +``` diff --git a/src/docs/@and_return.md b/src/docs/@and_return.md new file mode 100644 index 0000000..53dac9a --- /dev/null +++ b/src/docs/@and_return.md @@ -0,0 +1,48 @@ + Try.@and_return result -> result′ + +Evaluate `f(value)` if `result` is a "success" wrapping a `value`; otherwise, a "failure" +`value` as-is. + +| Invocation | Equivalent code | +|:--- |:--- | +| `@and_return Ok(value)` | `value` | +| `@and_return err::Err` | `return err` | +| `@and_return Some(value)` | `value` | +| `@and_return nothing` | `return nothing` | + +See also: [`@?`](@ref) [`and_then`](@ref), [`or_else`](@ref). + +# Extended help + +## Examples + +Let's define a function `nitems` that works like `length` but falls back to iteration-based +counting: + +```julia +using Try, TryExperimental + +function trygetnitems(xs) + Try.@and_return trygetlength(xs) + Ok(count(Returns(true), xs)) +end + +nitems(xs) = Try.unwrap(trygetnitems(xs)) + +nitems(1:3) + +# output +3 +``` + +`nitems` works with arbitrary iterator, including the ones that does not have `length`: + +```julia +ch = foldl(push!, 1:3; init = Channel{Int}(3)) +close(ch) + +nitems(ch) + +# output +3 +``` diff --git a/src/docs/and_then.md b/src/docs/and_then.md new file mode 100644 index 0000000..47a5472 --- /dev/null +++ b/src/docs/and_then.md @@ -0,0 +1,41 @@ + Try.and_then(f, result) -> result′ + Try.and_then(f) -> result -> result′ + +Evaluate `f(value)` if `result` is a "success" wrapping a `value`; otherwise, a "failure" +`value` as-is. + +| Invocation | Equivalent code | +|:--- |:--- | +| `and_then(f, Ok(value))` | `f(value)` | +| `and_then(f, err::Err)` | `err` | +| `and_then(f, Some(value))` | `f(value)` | +| `and_then(f, nothing)` | `nothing` | + +See also: [`@?`](@ref) [`@and_return`](@ref), [`or_else`](@ref). + +# Extended help + +## Examples + +```julia +using Try, TryExperimental + +try_map_prealloc(f, xs) = + Try.and_then(trygetlength(xs)) do n + Try.and_then(trygeteltype(xs)) do T + ys = Vector{T}(undef, n) + for (i, x) in zip(eachindex(ys), xs) + ys[i] = f(x) + end + return Ok(ys) + end + end + +Try.unwrap(try_map_prealloc(x -> x + 1, 1:3)) + +# output +3-element Vector{Int64}: + 2 + 3 + 4 +``` diff --git a/src/docs/or_else.md b/src/docs/or_else.md new file mode 100644 index 0000000..0936459 --- /dev/null +++ b/src/docs/or_else.md @@ -0,0 +1,47 @@ + Try.or_else(f, result) -> result′ + Try.or_else(f) -> result -> result′ + +Return `result` as-is if it is a "successful" value; otherwise, unwrap a "failure" value in +`result` and then evaluate `f` on it. + +| Invocation | Equivalent code | +|:--- |:--- | +| `or_else(f, ok::Ok)` | `ok` | +| `or_else(f, Err(value))` | `f(value)` | +| `or_else(f, some::Some)` | `some` | +| `or_else(f, nothing)` | `f(nothing)` | + +See also: [`@?`](@ref) [`@and_return`](@ref), [`and_then`](@ref). + +# Extended help + +## Examples + +Let's define a function `nitems` that works like `length` but falls back to iteration-based +counting: + +```julia +using Try, TryExperimental + +nitems(xs) = + Try.or_else(trygetlength(xs)) do _ + Ok(count(Returns(true), xs)) + end |> Try.unwrap + +nitems(1:3) + +# output +3 +``` + +`nitems` works with arbitrary iterator, including the ones that does not have `length`: + +```julia +ch = foldl(push!, 1:3; init = Channel{Int}(3)) +close(ch) + +nitems(ch) + +# output +3 +``` diff --git a/src/sugar.jl b/src/sugar.jl index 11e256e..d321fb5 100644 --- a/src/sugar.jl +++ b/src/sugar.jl @@ -35,27 +35,3 @@ macro or_else(f, ex) end end end - -const var"@_return" = var"@return" - -macro _return(ex) - quote - result = $(esc(ex)) - if Try.isok(result) - return result - else - Try.unwrap_err(result) - end - end -end - -macro return_err(ex) - quote - result = $(esc(ex)) - if Try.iserr(result) - return result - else - Try.unwrap(result) - end - end -end diff --git a/src/tools.jl b/src/tools.jl deleted file mode 100644 index 6bee22e..0000000 --- a/src/tools.jl +++ /dev/null @@ -1,33 +0,0 @@ -function Try.and_then(f::F) where {F} - function and_then_closure(result) - Try.and_then(f, result) - end -end - -Try.and_then(f, result::Ok)::AbstractResult = f(Try.unwrap(result)) -Try.and_then(_, result::Err) = result -function Try.and_then(f, result::ConcreteResult)::ConcreteResult - value = result.value - if value isa Ok - f(Try.unwrap(value)) - else - value - end -end - -function Try.or_else(f::F) where {F} - function or_else_closure(result) - Try.or_else(f, result) - end -end - -Try.or_else(_, result::Ok) = result -Try.or_else(f, result::Err)::AbstractResult = f(Try.unwrap_err(result)) -function Try.or_else(f, result::ConcreteResult)::ConcreteResult - value = result.value - if value isa Err - f(Try.unwrap_err(value)) - else - value - end -end diff --git a/test/TryTests/src/test_doctest.jl b/test/TryTests/src/test_doctest.jl index 96f9dcd..d21e8b3 100644 --- a/test/TryTests/src/test_doctest.jl +++ b/test/TryTests/src/test_doctest.jl @@ -1,11 +1,15 @@ module TestDoctest using Documenter -using Test using Try +using TryExperimental -function test() +function test_try() doctest(Try; manual = false) end +function test_tryexperimental() + doctest(TryExperimental; manual = false) +end + end # module diff --git a/test/TryTests/src/test_tools.jl b/test/TryTests/src/test_tools.jl index 46ee93b..f0224d2 100644 --- a/test/TryTests/src/test_tools.jl +++ b/test/TryTests/src/test_tools.jl @@ -4,6 +4,62 @@ using Test using Try using TryExperimental +function trygetnitems(xs) + Try.@and_return trygetlength(xs) + Ok(count(Returns(true), xs)) +end + +function nitems(xs) + Try.or_else(trygetlength(xs)) do _ + Ok(count(Returns(true), xs)) + end |> Try.unwrap +end + +function test_and_return() + @test Try.unwrap(trygetnitems(1:3)) == 3 + + ch = foldl(push!, 1:3; init = Channel{Int}(3)) + close(ch) + @test Try.unwrap(trygetnitems(ch)) == 3 +end + +function test_or_else() + @test nitems(1:3) == 3 + + ch = foldl(push!, 1:3; init = Channel{Int}(3)) + close(ch) + @test nitems(ch) == 3 +end + +try_map_prealloc(f, xs) = + Try.and_then(trygetlength(xs)) do n + Try.and_then(trygeteltype(xs)) do T + ys = Vector{T}(undef, n) + for (i, x) in zip(eachindex(ys), xs) + ys[i] = f(x) + end + return Ok(ys) + end + end + +function try_map_prealloc2(f, xs) + T = @? trygeteltype(xs) # macro-based short-circuiting + n = @? trygetlength(xs) + ys = Vector{T}(undef, n) + for (i, x) in zip(eachindex(ys), xs) + ys[i] = f(x) + end + return Ok(ys) +end + +function test_and_then() + @test Try.unwrap(try_map_prealloc(x -> x + 1, 1:3)) == 2:4 +end + +function test_or_return() + @test Try.unwrap(try_map_prealloc2(x -> x + 1, 1:3)) == 2:4 +end + function test_curry() value = tryconvert(String, 1) |>