diff --git a/base/exports.jl b/base/exports.jl index 306697bbf229f..537e4e92fb016 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -1320,6 +1320,7 @@ export # nullable types isnull, unsafe_get, + lift # Macros # parser internal diff --git a/base/nullable.jl b/base/nullable.jl index dfcec4f68dd3e..cdc132f077bb3 100644 --- a/base/nullable.jl +++ b/base/nullable.jl @@ -215,3 +215,138 @@ function hash(x::Nullable, h::UInt) return hash(x.value, h + nullablehash_seed) end end + +############################################################################## +## +## Standard lifting semantics +## +## For a function call f(xs...), return null if any x in xs is null; +## otherwise, return f applied to values of xs. +## +############################################################################## + +@inline function lift(f, x) + if null_safe_op(f, typeof(x)) + return @compat Nullable(f(x.value), !isnull(x)) + else + U = Core.Inference.return_type(f, Tuple{eltype(typeof(x))}) + if isnull(x) + return Nullable{U}() + else + return Nullable(f(unsafe_get(x))) + end + end +end + +@inline function lift(f, x1, x2) + if null_safe_op(f, typeof(x1), typeof(x2)) + return @compat Nullable( + f(x1.value, x2.value), !(isnull(x1) | isnull(x2)) + ) + else + U = Core.Inference.return_type( + f, Tuple{eltype(typeof(x1)), eltype(typeof(x2))} + ) + if isnull(x1) | isnull(x2) + return Nullable{U}() + else + return Nullable(f(unsafe_get(x1), unsafe_get(x2))) + end + end +end + +@inline function lift(f, xs...) + if null_safe_op(f, map(typeof, xs)...) + return @compat Nullable( + f(map(unsafe_get, xs)...), !(mapreduce(isnull, |, xs)) + ) + else + U = Core.Inference.return_type( + f, Tuple{map(x->eltype(typeof(x)), xs)...} + ) + if hasnulls(xs) + return Nullable{U}() + else + return Nullable(f(map(unsafe_get, xs)...)) + end + end +end + +############################################################################## +## +## Non-standard lifting semantics +## +############################################################################## + +# three-valued logic implementation +@inline function lift(::typeof(&), x, y)::Nullable{Bool} + return ifelse( isnull(x), + ifelse( isnull(y), + Nullable{Bool}(), # x, y null + ifelse( unsafe_get(y), + Nullable{Bool}(), # x null, y == true + Nullable(false) # x null, y == false + ) + ), + ifelse( isnull(y), + ifelse( unsafe_get(x), + Nullable{Bool}(), # x == true, y null + Nullable(false) # x == false, y null + ), + Nullable(unsafe_get(x) & unsafe_get(y)) # x, y not null + ) + ) +end + +# three-valued logic implementation +@inline function lift(::typeof(|), x, y)::Nullable{Bool} + return ifelse( isnull(x), + ifelse( isnull(y), + Nullable{Bool}(), # x, y null + ifelse( unsafe_get(y), + Nullable(true), # x null, y == true + Nullable{Bool}() # x null, y == false + ) + ), + ifelse( isnull(y), + ifelse( unsafe_get(x), + Nullable(true), # x == true, y null + Nullable{Bool}() # x == false, y null + ), + Nullable(unsafe_get(x) | unsafe_get(y)) # x, y not null + ) + ) +end + +# TODO: Decide on semantics for isequal and uncomment the following +# @inline function lift(::typeof(isequal), x, y) +# return ifelse( isnull(x), +# ifelse( isnull(y), +# true, # x, y null +# false # x null, y not null +# ), +# ifelse( isnull(y), +# false, # x not null, y null +# isequal(unsafe_get(x), unsafe_get(y)) # x, y not null +# ) +# ) +# end + +@inline function lift(::typeof(isless), x, y)::Bool + if null_safe_op(isless, typeof(x), typeof(y)) + return ifelse( isnull(x), + false, # x null + ifelse( isnull(y), + true, # x not null, y null + isless(unsafe_get(x), unsafe_get(y)) # x, y not null + ) + ) + else + return isnull(x) ? false : + isnull(y) ? true : isless(unsafe_get(x), unsafe_get(y)) + end +end + +@inline lift(::typeof(isnull), x) = isnull(x) +@inline lift(::typeof(get), x::Nullable) = get(x) +@inline lift(::typeof(get), x::Nullable, y) = get(x, y) diff --git a/test/nullable.jl b/test/nullable.jl index 819cd4a5871ce..9da998e329c58 100644 --- a/test/nullable.jl +++ b/test/nullable.jl @@ -387,3 +387,121 @@ end # issue #11675 @test repr(Nullable()) == "Nullable{Union{}}()" + +############################################################################## +## +## Test standard lifting semantics +## +############################################################################## + +types = [ + Float16, + Float32, + Float64, + Int128, + Int16, + Int32, + Int64, + Int8, + UInt16, + UInt32, + UInt64, + UInt8, +] + +f(x::Number) = 5 * x +f(x::Number, y::Number) = x + y +f(x::Number, y::Number, z::Number) = x + y * z + +for T in types + a = one(T) + x = Nullable{T}(a) + y = Nullable{T}() + + U1 = Core.Inference.return_type(f, Tuple{T}) + @test isequal(SQ.lift(f, x), Nullable(f(a))) + @test isequal(SQ.lift(f, y), Nullable{U1}()) + + U2 = Core.Inference.return_type(f, Tuple{T, T}) + @test isequal(SQ.lift(f, x, x), Nullable(f(a, a))) + @test isequal(SQ.lift(f, x, y), Nullable{U2}()) + + U3 = Core.Inference.return_type(f, Tuple{T, T, T}) + @test isequal(SQ.lift(f, x, x, x), Nullable(f(a, a, a))) + @test isequal(SQ.lift(f, x, y, x), Nullable{U3}()) +end + +############################################################################## +## +## Test non-standard lifting semantics +## +############################################################################## + +# three-valued logic + +# & truth table +v1 = SQ.lift(&, Nullable(true), Nullable(true)) +v2 = SQ.lift(&, Nullable(true), Nullable(false)) +v3 = SQ.lift(&, Nullable(true), Nullable{Bool}()) +v4 = SQ.lift(&, Nullable(false), Nullable(true)) +v5 = SQ.lift(&, Nullable(false), Nullable(false)) +v6 = SQ.lift(&, Nullable(false), Nullable{Bool}()) +v7 = SQ.lift(&, Nullable{Bool}(), Nullable(true)) +v8 = SQ.lift(&, Nullable{Bool}(), Nullable(false)) +v9 = SQ.lift(&, Nullable{Bool}(), Nullable{Bool}()) + +@test isequal(v1, Nullable(true)) +@test isequal(v2, Nullable(false)) +@test isequal(v3, Nullable{Bool}()) +@test isequal(v4, Nullable(false)) +@test isequal(v5, Nullable(false)) +@test isequal(v6, Nullable(false)) +@test isequal(v7, Nullable{Bool}()) +@test isequal(v8, Nullable(false)) +@test isequal(v9, Nullable{Bool}()) + +# | truth table +u1 = SQ.lift(|, Nullable(true), Nullable(true)) +u2 = SQ.lift(|, Nullable(true), Nullable(false)) +u3 = SQ.lift(|, Nullable(true), Nullable{Bool}()) +u4 = SQ.lift(|, Nullable(false), Nullable(true)) +u5 = SQ.lift(|, Nullable(false), Nullable(false)) +u6 = SQ.lift(|, Nullable(false), Nullable{Bool}()) +u7 = SQ.lift(|, Nullable{Bool}(), Nullable(true)) +u8 = SQ.lift(|, Nullable{Bool}(), Nullable(false)) +u9 = SQ.lift(|, Nullable{Bool}(), Nullable{Bool}()) + +@test isequal(u1, Nullable(true)) +@test isequal(u2, Nullable(true)) +@test isequal(u3, Nullable(true)) +@test isequal(u4, Nullable(true)) +@test isequal(u5, Nullable(false)) +@test isequal(u6, Nullable{Bool}()) +@test isequal(u7, Nullable(true)) +@test isequal(u8, Nullable{Bool}()) +@test isequal(u9, Nullable{Bool}()) + +# others + +x1 = Nullable(1) +x2 = Nullable(2) +y = Nullable{Int}() +z1 = 1 +z2 = 2 + +@test SQ.lift(isnull, x1) == false +@test SQ.lift(isnull, y) == true + +@test SQ.lift(isless, x1, y) == true +@test SQ.lift(isless, y, x1) == false +@test SQ.lift(isless, x1, x2) == true +@test SQ.lift(isless, x2, x1) == false +@test SQ.lift(isless, y, y) == false +@test SQ.lift(isless, x1, z2) == true +@test SQ.lift(isless, x2, z1) == false +@test SQ.lift(isless, z1, x2) == true +@test SQ.lift(isless, z2, x1) == false + +@test SQ.lift(get, x1) == 1 +@test_throws NullException SQ.lift(get, y) +@test SQ.lift(get, y, 1) == 1