Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize short-circuit APIs #24

Merged
merged 1 commit into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 9 additions & 7 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 = "<repository url>"
)=#
deploydocs(
repo = "github.com/tkf/Try.jl",
devbranch = "main",
push_preview = true,
# Ref:
# https://juliadocs.github.io/Documenter.jl/stable/lib/public/#Documenter.deploydocs
)
11 changes: 11 additions & 0 deletions docs/src/experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Experimental

## [Customizing short-circuit evaluation](@id customize-short-circuit)

```@docs
TryExperimental.branch
TryExperimental.Break
TryExperimental.Continue
TryExperimental.resultof
TryExperimental.valueof
```
10 changes: 10 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
10 changes: 10 additions & 0 deletions lib/TryExperimental/src/TryExperimental.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
baremodule TryExperimental

import Try

module InternalPrelude
include("prelude.jl")
end # module InternalPrelude
Expand All @@ -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
Expand Down Expand Up @@ -64,4 +72,6 @@ include("maybe.jl")
end
end # module Maybe

Internal.Try.Internal.@define_docstrings

end # baremodule TryExperimental
6 changes: 3 additions & 3 deletions lib/TryExperimental/src/base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/TryExperimental/src/docs/Break.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TryExperimental.Break(result)
1 change: 1 addition & 0 deletions lib/TryExperimental/src/docs/Continue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TryExperimental.Continue(result)
5 changes: 5 additions & 0 deletions lib/TryExperimental/src/docs/branch.md
Original file line number Diff line number Diff line change
@@ -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`.
5 changes: 5 additions & 0 deletions lib/TryExperimental/src/docs/resultof.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lib/TryExperimental/src/docs/valueof.md
Original file line number Diff line number Diff line change
@@ -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
14 changes: 8 additions & 6 deletions src/Try.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
baremodule Try

export Ok, Err, Result
export @?, Ok, Err, Result

using Base: Base, Exception

Expand Down Expand Up @@ -46,6 +46,7 @@ function enable_errortrace end
function disable_errortrace end

function istryable end
function var"@function" end

# Core exceptions
struct IsOkError <: Exception
Expand All @@ -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,
Expand All @@ -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
Expand Down
97 changes: 97 additions & 0 deletions src/branch.jl
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions src/docs/@?.md
Original file line number Diff line number Diff line change
@@ -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
```
48 changes: 48 additions & 0 deletions src/docs/@and_return.md
Original file line number Diff line number Diff line change
@@ -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
```
Loading