Skip to content

Commit

Permalink
Generalize short-circuit APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
tkf committed Mar 21, 2022
1 parent e158820 commit 85ab1ec
Show file tree
Hide file tree
Showing 21 changed files with 410 additions and 77 deletions.
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
)
15 changes: 15 additions & 0 deletions docs/src/experimental.md
Original file line number Diff line number Diff line change
@@ -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
```
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

0 comments on commit 85ab1ec

Please sign in to comment.