Skip to content

Commit

Permalink
Implement operators for Nullable with lifting semantics
Browse files Browse the repository at this point in the history
Use fast path without a branch for types with unchecked arithmetic
(for which the operation can be computed even when value is missing)
and a slow path for other types. The new null_safe_op() function
allows custom types to opt-in to the fast path when possible.
Also use this strategy in isequal(), which keeps its current
(non-lifting) behavior.
  • Loading branch information
nalimilan committed Jun 17, 2016
1 parent f143ae0 commit 62c3994
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 12 deletions.
1 change: 1 addition & 0 deletions base/bool.jl
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ cld(x::Bool, y::Bool) = div(x,y)
rem(x::Bool, y::Bool) = y ? false : throw(DivideError())
mod(x::Bool, y::Bool) = rem(x,y)

promote_op(op, ::Type{Bool}) = typeof(op(true))
promote_op(op, ::Type{Bool}, ::Type{Bool}) = typeof(op(true, true))
promote_op(::typeof(^), ::Type{Bool}, ::Type{Bool}) = Bool
promote_op{T<:Integer}(::typeof(^), ::Type{Bool}, ::Type{T}) = Bool
100 changes: 88 additions & 12 deletions base/nullable.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,6 @@ end

isnull(x::Nullable) = x.isnull

function isequal(x::Nullable, y::Nullable)
if x.isnull && y.isnull
return true
elseif x.isnull || y.isnull
return false
else
return isequal(x.value, y.value)
end
end

==(x::Nullable, y::Nullable) = throw(NullException())

const nullablehash_seed = UInt === UInt64 ? 0x932e0143e51d0171 : 0xe51d0171

function hash(x::Nullable, h::UInt)
Expand All @@ -78,3 +66,91 @@ function hash(x::Nullable, h::UInt)
return hash(x.value, h + nullablehash_seed)
end
end


## Operators

"""
null_safe_op(f::Any, ::Type)::Bool
null_safe_op(f::Any, ::Type, ::Type)::Bool
Returns whether an operation `f` can safely be applied to any value of the passed type(s).
Returns `false` by default.
Custom types should implement methods for some or all operations `f` when applicable:
returning `true` means that the operation may be called on any value without
throwing an error. In particular, this means that the operation can be applied on
the whole domain of the type *and on uninitialized objects*. Though returning invalid or
nonsensical results is not a problem. As a general rule, these proporties are only true for
unchecked operations on `isbits` types.
Types declared as safe can benefit from higher performance for operations on nullable: by
always computing the result even for null values, a branch is avoided, which helps
vectorization.
"""
null_safe_op(f::Any, ::Type) = false
null_safe_op(f::Any, ::Type, ::Type) = false

typealias UncheckedTypes Union{Checked.SignedInt, Checked.UnsignedInt}

# Unary operators

for op in (:+, :-, :~)
@eval begin
null_safe_op{T<:UncheckedTypes}(::typeof($op), ::Type{T}) = true
end
end

null_safe_op(::typeof(!), ::Type{Bool}) = true

for op in (:+, :-, :!, :~)
@eval begin
@inline function $op{S}(x::Nullable{S})
R = promote_op($op, S)
if null_safe_op($op, S)
Nullable{R}($op(x.value), x.isnull)
else
x.isnull ? Nullable{R}() :
Nullable{R}($op(x.value))
end
end
end
end

# Binary operators

# Note this list does not include ^, ÷ and %
# Operations between signed and unsigned types are not safe: promotion to unsigned
# gives an InexactError for negative numbers
for op in (:+, :-, :*, :/, :&, :|, :<<, :>>, :(>>>),
:(==), :<, :>, :<=, :>=)
@eval begin
null_safe_op{S<:Checked.SignedInt,
T<:Checked.SignedInt}(::typeof($op), ::Type{S}, ::Type{T}) = true
null_safe_op{S<:Checked.UnsignedInt,
T<:Checked.UnsignedInt}(::typeof($op), ::Type{S}, ::Type{T}) = true
end
end

for op in (:+, :-, :*, :/, :%, :÷, :&, :|, :^, :<<, :>>, :(>>>),
:(==), :<, :>, :<=, :>=)
@eval begin
@inline function $op{S,T}(x::Nullable{S}, y::Nullable{T})
R = promote_op($op, S, T)
if null_safe_op($op, S, T)
Nullable{R}($op(x.value, y.value), x.isnull | y.isnull)
else
(x.isnull | y.isnull) ? Nullable{R}() :
Nullable{R}($op(x.value, y.value))
end
end
end
end

@inline function isequal{S,T}(x::Nullable{S}, y::Nullable{T})
if null_safe_op(isequal, S, T)
(x.isnull & y.isnull) | ((!x.isnull & !y.isnull) & isequal(x.value, y.value))
else
(x.isnull & y.isnull) || ((!x.isnull & !y.isnull) && isequal(x.value, y.value))
end
end
154 changes: 154 additions & 0 deletions test/nullable.jl
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,160 @@ for T in types
@test hash(x3) != hash(x4)
end


## operators

srand(1)

# check for fast path (null-safe combinations of types)
for S in Union{Base.Checked.SignedInt, Base.Checked.UnsignedInt}.types,
T in Union{Base.Checked.SignedInt, Base.Checked.UnsignedInt}.types
# mixing signed and unsigned types is unsafe (slow path tested below)
((S <: Signed) $ (T <: Signed)) && continue

u0 = zero(S)
u1 = one(S)
u2 = rand(S)

v0 = zero(T)
v1 = one(T)
v2 = rand(T)
# unary operators
for op in (+, -, ~)
@test op(Nullable(u0)) === Nullable(op(u0))
@test op(Nullable(u1)) === Nullable(op(u1))
@test op(Nullable(u2)) === Nullable(op(u2))
@test op(Nullable(u0, true)) === Nullable(op(u0), true)
end

for u in (u0, u1, u2), v in (v0, v1, v2)
# safe binary operators: === checks that the fast-path was taken (no branch)
for op in (+, -, *, /, &, |, >>, <<, >>>,
<, >, <=, >=)
@test op(Nullable(u), Nullable(v)) === Nullable(op(u, v))
@test op(Nullable(u, true), Nullable(v, true)) === Nullable(op(u, v), true)
@test op(Nullable(u), Nullable(v, true)) === Nullable(op(u, v), true)
@test op(Nullable(u, true), Nullable(v)) === Nullable(op(u, v), true)
end

# unsafe binary operators: we cannot use === as the underlying value is undefined
# ^
if S <: Integer
@test_throws DomainError Nullable(u)^Nullable(-signed(one(v)))
end
@test isequal(Nullable(u)^Nullable(2*one(T)), Nullable(u^(2*one(T))))
R = Base.promote_op(^, S, T)
x = Nullable(u, true)^Nullable(-abs(v), true)
@test isnull(x) && eltype(x) === R
x = Nullable(u, false)^Nullable(-abs(v), true)
@test isnull(x) && eltype(x) === R
x = Nullable(u, true)^Nullable(-abs(v), false)
@test isnull(x) && eltype(x) === R

# ÷ and %
for op in (÷, %)
if T <: Integer && v == 0
@test_throws DivideError op(Nullable(u), Nullable(v))
else
@test isequal(op(Nullable(u), Nullable(v)), Nullable(op(u, v)))
end
R = Base.promote_op(op, S, T)
x = op(Nullable(u, true), Nullable(v, true))
@test isa(x, Nullable{R}) && isnull(x)
x = op(Nullable(u, false), Nullable(v, true))
@test isa(x, Nullable{R}) && isnull(x)
x = op(Nullable(u, true), Nullable(v, false))
@test isa(x, Nullable{R}) && isnull(x)
end
end
end

@test !Nullable(true) === Nullable(false)
@test !Nullable(false) === Nullable(true)
@test !(Nullable(true, true)) === Nullable(false, true)
@test !(Nullable(false, true)) === Nullable(true, true)

# test all types (including null-unsafe types and combinations of types)
for S in Union{Base.Checked.SignedInt, Base.Checked.UnsignedInt, BigInt, BigFloat}.types,
T in Union{Base.Checked.SignedInt, Base.Checked.UnsignedInt, BigInt, BigFloat}.types
u0 = zero(S)
u1 = one(S)
u2 = S <: Union{BigInt, BigFloat} ? S(rand(Int128)) : rand(S)

v0 = zero(T)
v1 = one(T)
v2 = T <: Union{BigInt, BigFloat} ? T(rand(Int128)) : rand(T)

v2 > 5 && (v2 = T(5)) # Work around issue #16989

# unary operators
for op in (+, -, ~) # !
T <: BigFloat && op == (~) && continue
R = Base.promote_op(op, T)
x = op(Nullable(v0))
@test isa(x, Nullable{R}) && isequal(x, Nullable(op(v0)))
x = op(Nullable(v1))
@test isa(x, Nullable{R}) && isequal(x, Nullable(op(v1)))
x = op(Nullable(v2))
@test isa(x, Nullable{R}) && isequal(x, Nullable(op(v2)))
x = op(Nullable(v0, true))
@test isa(x, Nullable{R}) && isnull(x)
x = op(Nullable{R}())
@test isa(x, Nullable{R}) && isnull(x)
end

for u in (u0, u1, u2), v in (v0, v1, v2)
# safe binary operators
for op in (+, -, *, /, &, |, >>, <<, >>>,
<, >, <=, >=)
(T <: BigFloat || S <: BigFloat) && op in (&, |, >>, <<, >>>) && continue
if S <: Unsigned || T <: Unsigned
@test isequal(op(Nullable(abs(u)), Nullable(abs(v))), Nullable(op(abs(u), abs(v))))
else
@test isequal(op(Nullable(u), Nullable(v)), Nullable(op(u, v)))
end
R = Base.promote_op(op, S, T)
x = op(Nullable(u, true), Nullable(v, true))
@test isa(x, Nullable{R}) && isnull(x)
x = op(Nullable(u), Nullable(v, true))
@test isa(x, Nullable{R}) && isnull(x)
x = op(Nullable(u, true), Nullable(v))
@test isa(x, Nullable{R}) && isnull(x)
end

# unsafe binary operators
# ^
if S <: Integer && !(T <: BigFloat)
@test_throws DomainError Nullable(u)^Nullable(-signed(one(v)))
end
@test isequal(Nullable(u)^Nullable(2*one(T)), Nullable(u^(2*one(T))))
R = Base.promote_op(^, S, T)
x = Nullable(u, true)^Nullable(-abs(v), true)
@test isnull(x) && eltype(x) === R
x = Nullable(u, false)^Nullable(-abs(v), true)
@test isnull(x) && eltype(x) === R
x = Nullable(u, true)^Nullable(-abs(v), false)
@test isnull(x) && eltype(x) === R

# ÷ and %
for op in (÷, %)
if S <: Integer && T <: Integer && v == 0
@test_throws DivideError op(Nullable(u), Nullable(v))
else
@test isequal(op(Nullable(u), Nullable(v)), Nullable(op(u, v)))
end
R = Base.promote_op(op, S, T)
x = op(Nullable(u, true), Nullable(v, true))
@test isnull(x) && eltype(x) === R
x = op(Nullable(u, false), Nullable(v, true))
@test isnull(x) && eltype(x) === R
x = op(Nullable(u, true), Nullable(v, false))
@test isnull(x) && eltype(x) === R
end
end
end


type TestNType{T}
v::Nullable{T}
end
Expand Down

0 comments on commit 62c3994

Please sign in to comment.