Skip to content

Commit

Permalink
Merge pull request #15 from invenia/aa/the-future
Browse files Browse the repository at this point in the history
Updates for Julia 0.7 and 1.0
  • Loading branch information
iamed2 authored Sep 25, 2018
2 parents 6deb3a1 + c8f33a4 commit 24e0aa7
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 79 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ language: julia
os:
- linux
julia:
- 0.6
- 0.7
- 1.0
- nightly
Expand Down
100 changes: 48 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,70 +67,66 @@ ErrorResult(Int64, DivideError())
Using the function above, we can use `@code_warntype` to verify that the compiler is doing what we desire:

```julia
julia> @code_warntype integer_division(3,2)
Variables:
#self#::#integer_division
x::Int64
y::Int64

Body:
begin
unless (y::Int64 === 0)::Bool goto 4 # line 3:
return $(Expr(:new, ResultTypes.Result{Int64,DivideError}, :($(Expr(:new, Nullable{Int64}, false))), :($(Expr(:new, Nullable{DivideError}, true, :($(QuoteNode(DivideError()))))))))
4: # line 5:
SSAValue(1) = (Base.checked_sdiv_int)(x::Int64, y::Int64)::Int64
return $(Expr(:new, ResultTypes.Result{Int64,DivideError}, :($(Expr(:new, Nullable{Int64}, true, SSAValue(1)))), :($(Expr(:new, Nullable{DivideError}, false)))))
end::ResultTypes.Result{Int64,DivideError}
julia> @code_warntype integer_division(3, 2)
Body::Result{Int64,DivideError}
2 1%1 = (y === 0)::Bool │╻ ==
└── goto #3 if not %1 │
3 2%3 = %new(Result{Int64,DivideError}, nothing, $(QuoteNode(DivideError())))::Result{Int64,DivideError} │╻╷ convert
└── return %3
5 3%5 = (Base.checked_sdiv_int)(x, y)::Int64 │╻ div
%6 = %new(Some{Int64}, %5)::Some{Int64} ││╻╷╷╷ Type
%7 = %new(Result{Int64,DivideError}, %6, nothing)::Result{Int64,DivideError} │││
└── return %7
```

### Experimental

Suppose we have two versions of a function where one returns a value or throws an exception and the other returns a `Result` type.
We want to call the function and return the value if present or a default value if there was an error.
For this example we can use `div` and our `integer_division` function as a microbenchmark (they are too simple to provide a realistic use case).
We'll use `@noinline` to ensure the functions don't get inlined, which will make the benchmarks more comparable.

Here's our wrapping function for `div`:

```julia
function func1(x,y)
local z
try
z = div(x,y)
catch e
z = 0
end

return z
@noinline function func1(x, y)
local z
try
z = div(x, y)
catch e
z = 0
end
return z
end
```

and for `integer_division`:

```julia
function func2(x, y)
r = integer_division(x,y)
if iserror(r)
return 0
else
return unwrap(r)
end
@noinline function func2(x, y)
r = integer_division(x, y)
if ResultTypes.iserror(r)
return 0
else
return unwrap(r)
end
end
```

Here are some benchmark results in the average case (on one machine), using [BenchmarkTools.jl](https://github.com/JuliaCI/BenchmarkTools.jl):

```julia
julia> using BenchmarkTools
julia> using BenchmarkTools, Statistics

julia> t1 = @benchmark for i = 1:10 func1(3, i % 2) end
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 299.175 μs (0.00% GC)
median time: 340.283 μs (0.00% GC)
mean time: 368.364 μs (0.00% GC)
maximum time: 1.681 ms (0.00% GC)
minimum time: 121.664 μs (0.00% GC)
median time: 122.652 μs (0.00% GC)
mean time: 124.350 μs (0.00% GC)
maximum time: 388.198 μs (0.00% GC)
--------------
samples: 10000
evals/sample: 1
Expand All @@ -140,17 +136,17 @@ BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 98.093 ns (0.00% GC)
median time: 111.542 ns (0.00% GC)
mean time: 120.148 ns (0.00% GC)
maximum time: 537.122 ns (0.00% GC)
minimum time: 18.853 ns (0.00% GC)
median time: 21.078 ns (0.00% GC)
mean time: 21.183 ns (0.00% GC)
maximum time: 275.057 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 946
evals/sample: 997

julia> judge(mean(t2), mean(t1))
BenchmarkTools.TrialJudgement:
time: -99.97% => improvement (5.00% tolerance)
time: -99.98% => improvement (5.00% tolerance)
memory: +0.00% => invariant (1.00% tolerance)
```

Expand All @@ -164,30 +160,30 @@ BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 219.074 ns (0.00% GC)
median time: 236.420 ns (0.00% GC)
mean time: 252.231 ns (0.00% GC)
maximum time: 1.049 μs (0.00% GC)
minimum time: 115.060 ns (0.00% GC)
median time: 118.042 ns (0.00% GC)
mean time: 118.616 ns (0.00% GC)
maximum time: 279.901 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 501
evals/sample: 918

julia> t2 = @benchmark for i = 1:10 func2(3, 1) end
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 150.658 ns (0.00% GC)
median time: 170.757 ns (0.00% GC)
mean time: 181.448 ns (0.00% GC)
maximum time: 1.096 μs (0.00% GC)
minimum time: 28.775 ns (0.00% GC)
median time: 30.516 ns (0.00% GC)
mean time: 31.290 ns (0.00% GC)
maximum time: 74.936 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 555
evals/sample: 995

julia> judge(mean(t2), mean(t1))
BenchmarkTools.TrialJudgement:
time: -28.06% => improvement (5.00% tolerance)
time: -73.62% => improvement (5.00% tolerance)
memory: +0.00% => invariant (1.00% tolerance)
```

Expand Down
4 changes: 1 addition & 3 deletions REQUIRE
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
julia 0.6
Nullables 0.0.3
Compat 0.33
julia 0.7
40 changes: 20 additions & 20 deletions src/ResultTypes.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
__precompile__()

module ResultTypes

using Nullables
import Base: convert

export Result, ErrorResult, unwrap, unwrap_error

struct Result{T, E <: Exception}
result::Nullable{T}
error::Nullable{E}
result::Union{Some{T}, Nothing}
error::Union{E, Nothing}
end

"""
Expand All @@ -19,12 +14,17 @@ Create a `Result` that could hold a value of type `T` or an exception of type `E
store `val` in it.
If the exception type is not provided, the supertype `Exception` is used as `E`.
"""
Result(x::T) where {T} = Result{T, Exception}(Nullable{T}(x), Nullable{Exception}())
Result(x::T) where {T} = Result{T, Exception}(Some(x), nothing)

function Result(x::T, ::Type{E}) where {T, E <: Exception}
return Result{T, E}(Nullable{T}(x), Nullable{E}())
return Result{T, E}(Some(x), nothing)
end

# As of Julia 0.7, constructors no longer fall back to convert methods, so we can
# set that up manually to happen. Constructor methods that don't make sense will
# helpfully produce convert MethodErrors.
Result{T, E}(x) where {T, E <: Exception} = convert(Result{T, E}, x)

"""
ErrorResult(::Type{T}, exception::E) -> Result{T, E}
ErrorResult(::Type{T}, exception::AbstractString="") -> Result{T, ErrorException}
Expand All @@ -38,11 +38,11 @@ If the type argument is not provided, `Any` is used.
`ErrorResult` is a convenience function for creating a `Result` and is not its own type.
"""
function ErrorResult(::Type{T}, e::E) where {T, E <: Exception}
return Result{T, E}(Nullable{T}(), Nullable{E}(e))
return Result{T, E}(nothing, e)
end

function ErrorResult(::Type{T}, e::AbstractString="") where T
return Result{T, ErrorException}(Nullable{T}(), Nullable(ErrorException(e)))
return Result{T, ErrorException}(nothing, ErrorException(e))
end

ErrorResult(e::Union{Exception, AbstractString}="") = ErrorResult(Any, e)
Expand All @@ -61,10 +61,10 @@ The two-argument form of `unwrap` calls `unwrap` on its second argument, then co
type `T`.
"""
function unwrap(r::Result{T, E})::T where {T, E <: Exception}
if !isnull(r.result)
return get(r.result)
elseif !isnull(r.error)
throw(get(r.error))
if r.result !== nothing
return something(r.result)
elseif r.error !== nothing
throw(r.error)
else
error("Empty Result{$T, $E} type")
end
Expand All @@ -87,8 +87,8 @@ If `result` holds a value instead, throw an exception.
If `unwrap_error`'s argument is an `Exception`, that exception is returned.
"""
function unwrap_error(r::Result{T, E})::E where {T, E <: Exception}
if !isnull(r.error)
return get(r.error)
if r.error !== nothing
return r.error
else
error("$r is not an ErrorResult")
end
Expand All @@ -114,11 +114,11 @@ function Base.convert(::Type{Result{S, E}}, r::Result) where {S, E <: Exception}
end

function Base.convert(::Type{Result{S, E}}, x::T) where {T, S, E <: Exception}
return Result{S, E}(Nullable{S}(convert(S, x)), Nullable{E}())
return Result{S, E}(Some(convert(S, x)), nothing)
end

function Base.convert(::Type{Result{T, E}}, e::E) where {T, E <: Exception}
return Result{T, E}(Nullable{T}(), Nullable{E}(e))
return Result{T, E}(nothing, e)
end

function Base.show(io::IO, r::Result{T, E}) where {T, E <: Exception}
Expand All @@ -137,7 +137,7 @@ If `x` is an `ErrorResult` (a `Result` containing an `Exception`), return `true`
Return `false` in all other cases.
"""
iserror(e::Exception) = true
iserror(r::Result) = !isnull(r.error)
iserror(r::Result) = r.error !== nothing
iserror(x) = false

end # module
5 changes: 2 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using ResultTypes
using Compat.Test
using Nullables
using Test

@testset "ResultTypes" begin

Expand All @@ -26,7 +25,7 @@ using Nullables
end

@testset "Malformed result" begin
x = Result(Nullable{Int}(), Nullable{DivideError}())
x = Result{Int, Exception}(nothing, nothing)
@test_throws ErrorException unwrap(x)
@test_throws ErrorException unwrap_error(x)
end
Expand Down

0 comments on commit 24e0aa7

Please sign in to comment.