From 968ad8e36097f058a4a995503c3fe3f00ef73a79 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 6 Jan 2022 18:14:53 -0500 Subject: [PATCH] Add inferrability example --- README.md | 32 +++++++++++++++++++++---- examples/inferrability.jl | 14 +++++++++++ src/Try.jl | 7 ++++-- src/base.jl | 6 +++++ src/core.jl | 30 +++++++++++++++-------- test/TryTests/src/TryTests.jl | 1 + test/TryTests/src/test_inferrability.jl | 16 +++++++++++++ 7 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 examples/inferrability.jl create mode 100644 test/TryTests/src/test_inferrability.jl diff --git a/README.md b/README.md index 582e4d3..1696dee 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ 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 @@ -25,7 +26,8 @@ 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 @@ -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 diff --git a/examples/inferrability.jl b/examples/inferrability.jl new file mode 100644 index 0000000..8282e18 --- /dev/null +++ b/examples/inferrability.jl @@ -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 diff --git a/src/Try.jl b/src/Try.jl index f2dd893..1565c6a 100644 --- a/src/Try.jl +++ b/src/Try.jl @@ -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{}} @@ -83,7 +85,8 @@ using ..Try: Err, Ok, Result, - Try + Try, + _ConcreteResult using Base.Meta: isexpr diff --git a/src/base.jl b/src/base.jl index b4be56c..c21c1ce 100644 --- a/src/base.jl +++ b/src/base.jl @@ -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 diff --git a/src/core.jl b/src/core.jl index 1bd8984..ced0655 100644 --- a/src/core.jl +++ b/src/core.jl @@ -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)) diff --git a/test/TryTests/src/TryTests.jl b/test/TryTests/src/TryTests.jl index f239226..b39dd58 100644 --- a/test/TryTests/src/TryTests.jl +++ b/test/TryTests/src/TryTests.jl @@ -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 diff --git a/test/TryTests/src/test_inferrability.jl b/test/TryTests/src/test_inferrability.jl new file mode 100644 index 0000000..04afa59 --- /dev/null +++ b/test/TryTests/src/test_inferrability.jl @@ -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