Skip to content

Commit

Permalink
Add inferrability example (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkf authored Jan 7, 2022
1 parent 64b0977 commit 970e03c
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 16 deletions.
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ Features:
```julia
julia> using Try

julia> result = Try.getindex(Dict(:a => 111), :a);
julia> result = Try.getindex(Dict(:a => 111), :a)
Try.Ok: 111

julia> Try.isok(result)
true

julia> Try.unwrap(result)
111

julia> result = Try.getindex(Dict(:a => 111), :b);
julia> result = Try.getindex(Dict(:a => 111), :b)
Try.Err: KeyError: key :b not found

julia> Try.iserr(result)
true
Expand Down Expand Up @@ -79,8 +81,30 @@ mymap(x -> x + 1, (x for x in 1:5 if isodd(x)))

Try.jl provides an API inspired by Rust's `Result` type. However, to fully
unlock the power of Julia, Try.jl uses the *small `Union` types* instead of a
concretely typed sum type. Furthermore, it optionally supports concretely-typed
returned value when `Union` is not appropriate.
concretely typed `struct` type. This is essential for idiomatic clean
high-level Julia code that avoids computing output type manually. However, all
previous attempts in this space (such as
[ErrorTypes.jl](https://github.com/jakobnissen/ErrorTypes.jl),
[ResultTypes.jl](https://github.com/iamed2/ResultTypes.jl), and
[Expect.jl](https://github.com/KristofferC/Expect.jl)) use a `struct` type for
representing the result value (see
[`ErrorTypes.Result`](https://github.com/jakobnissen/ErrorTypes.jl/blob/c3a7d529716ebfa3ee956049f77f606b6c00700b/src/ErrorTypes.jl#L45-L47),
[`ResultTypes.Result`](https://github.com/iamed2/ResultTypes.jl/blob/42ebadf4d859964efa36ebccbeed3d5b65f3e9d9/src/ResultTypes.jl#L5-L8),
and
[`Expect.Expected`](https://github.com/KristofferC/Expect.jl/blob/6834049306c2b53c1666cbed504655e36b56e3b4/src/Expect.jl#L6-L9)).
Using a concretely typed `struct` as returned type has some benefits in that it
is easy to control the result of type inference. However, this is at the cost
of losing the opportunity for the compiler to eliminate the success and/or
failure branches. A similar optimization can happen in principle with the
concrete `struct` approach with some aggressive (post-inference) inlining,
scalar replacement of aggregate, and dead code elimination. However, since type
inference is the main driving force in the inter-procedural analysis of the
Julia compiler, `Union` return type is likely to continue to be the most
effective way to communicate the intent of the code with the compiler (e.g., if
a function call always succeeds, return an `Ok{T}`). (That said, Try.jl also
contains supports for concretely-typed returned value when `Union` is not
appropriate. This is for experimenting if such a manual "type-stabilization" is
a viable approach and if providing a seamless interop API is possible.)

A potential usability issue for using the `Result` type is that the detailed
context of the error is lost by the time the user received an error. This makes
Expand Down
14 changes: 14 additions & 0 deletions examples/inferrability.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module UnionTyped
using Try
g(xs) = Ok(xs)
f(xs) = g(xs) |> Try.and_then(xs -> Try.getindex(xs, 1)) |> Try.ok
end # module UnionTyped

module ConcretelyTyped
using Try
g(xs) = Try.ConcreteOk(xs)
function trygetfirst(xs)::Try.ConcreteResult{eltype(xs),BoundsError}
Try.getindex(xs, 1)
end
f(xs) = g(xs) |> Try.and_then(trygetfirst) |> Try.ok
end # module
7 changes: 5 additions & 2 deletions src/Try.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ end

const DynamicResult{T,E} = Union{Ok{T},Err{E}}

function _ConcreteResult end

struct ConcreteResult{T,E<:Exception} <: AbstractResult{T,E}
value::DynamicResult{T,E}
ConcreteResult{T,E}(value::DynamicResult{T,E}) where {T,E<:Exception} = new{T,E}(value)
global _ConcreteResult(::Type{T}, ::Type{E}, value) where {T,E} = new{T,E}(value)
end

const ConcreteOk{T} = ConcreteResult{T,Union{}}
Expand Down Expand Up @@ -83,7 +85,8 @@ using ..Try:
Err,
Ok,
Result,
Try
Try,
_ConcreteResult

using Base.Meta: isexpr

Expand Down
6 changes: 6 additions & 0 deletions src/base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ end
return Ok(v)
end

@inline function Try.getindex(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
Expand Down
30 changes: 20 additions & 10 deletions src/core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,36 @@ end
Base.convert(::Type{Ok{T}}, ok::Ok) where {T} = Ok{T}(ok.value)
Base.convert(::Type{Err{E}}, err::Err) where {E} = Err{E}(err.value)

_concrete(result::Ok) = _ConcreteResult(Try.oktype(result), Union{}, result)
_concrete(result::Err) = _ConcreteResult(Union{}, Try.errtype(result), result)

Try.ConcreteOk(value) = _concrete(Ok(value))
Try.ConcreteOk{T}(value) where {T} = _concrete(Ok{T}(value))
Try.ConcreteErr(value) = _concrete(Err(value))
Try.ConcreteErr{E}(value) where {E} = _concrete(Err{E}(value))

Base.convert(::Type{ConcreteResult{T,E}}, result::Ok) where {T,E} =
_ConcreteResult(T, E, convert(Ok{T}, result))
Base.convert(::Type{ConcreteResult{T}}, result::Ok) where {T} =
_ConcreteResult(T, Union{}, convert(Ok{T}, result))

Base.convert(::Type{ConcreteResult{T,E}}, result::Err) where {T,E} =
_ConcreteResult(T, E, convert(Err{E}, result))
Base.convert(::Type{ConcreteResult{<:Any,E}}, result::Err) where {E} =
_ConcreteResult(Union{}, E, convert(Err{E}, result))

function Base.convert(
::Type{ConcreteResult{T,E}},
result::ConcreteResult{T′,E′},
) where {T,E,T′<:T,E′<:E}
value = result.value
if value isa Ok
return ConcreteResult{T,E}(Ok{T}(value.value))
return _ConcreteResult(T, E, Ok{T}(value.value))
else
return ConcreteResult{T,E}(Err{E}(value.value))
return _ConcreteResult(T, E, Err{E}(value.value))
end
end

Base.convert(::Type{ConcreteResult{T,E}}, result::ConcreteOk) where {T,E} =
ConcreteResult{T,E}(convert(Ok{T}, value.value))

Base.convert(::Type{ConcreteResult{T,E}}, result::ConcreteErr) where {T,E} =
ConcreteResult{T,E}(convert(Err{E}, value.value))

Try.ConcreteOk{T}(value) where {T} = Try.ConcreteOk{T}(Ok{T}(value))
Try.ConcreteErr{E}(value) where {E} = Try.ConcreteErr{E}(Err{E}(value))
Try.oktype(::Type{R}) where {T,R<:AbstractResult{T}} = T
Try.oktype(result::AbstractResult) = Try.oktype(typeof(result))

Expand Down
1 change: 1 addition & 0 deletions test/TryTests/src/TryTests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module TryTests
include("test_base.jl")
include("test_errortrace.jl")
include("test_tools.jl")
include("test_inferrability.jl")
include("test_doctest.jl")

end # module TryTests
16 changes: 16 additions & 0 deletions test/TryTests/src/test_inferrability.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module TestInferrability

using Test

include("../../../examples/inferrability.jl")

should_test_module() = lowercase(get(ENV, "JULIA_PKGEVAL", "false")) != "true"

function test()
@test @inferred(UnionTyped.f((111,))) == Some(111)
@test @inferred(UnionTyped.f(())) === nothing
@test ConcretelyTyped.f((111,)) == Some(111)
@test ConcretelyTyped.f(()) === nothing
end

end # module

0 comments on commit 970e03c

Please sign in to comment.