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

Add inferrability example #3

Merged
merged 1 commit into from
Jan 7, 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
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