Skip to content

Commit

Permalink
Implement higher-order lifting
Browse files Browse the repository at this point in the history
  • Loading branch information
davidagold committed Oct 20, 2016
1 parent efd830a commit beaf29c
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 0 deletions.
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,7 @@ export
# nullable types
isnull,
unsafe_get,
lift

# Macros
# parser internal
Expand Down
135 changes: 135 additions & 0 deletions base/nullable.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
118 changes: 118 additions & 0 deletions test/nullable.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit beaf29c

Please sign in to comment.