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

RFC: Implement operators on Nullable with lifting semantics #16988

Closed
wants to merge 2 commits into from
Closed
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
6 changes: 6 additions & 0 deletions base/bool.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ typemax(::Type{Bool}) = true
(|)(x::Bool, y::Bool) = box(Bool,or_int(unbox(Bool,x),unbox(Bool,y)))
($)(x::Bool, y::Bool) = (x!=y)

>>(x::Bool, c::Unsigned) = Int(x) >> c
<<(x::Bool, c::Unsigned) = Int(x) << c
>>>(x::Bool, c::Unsigned) = Int(x) >>> c

signbit(x::Bool) = false
sign(x::Bool) = x
abs(x::Bool) = x
Expand Down Expand Up @@ -60,6 +64,8 @@ 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{T}(op::Type{T}, ::Type{Bool}) = T # to fix ambiguity
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