Skip to content

Commit beaf29c

Browse files
committed
Implement higher-order lifting
1 parent efd830a commit beaf29c

File tree

3 files changed

+254
-0
lines changed

3 files changed

+254
-0
lines changed

base/exports.jl

+1
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,7 @@ export
13201320
# nullable types
13211321
isnull,
13221322
unsafe_get,
1323+
lift
13231324

13241325
# Macros
13251326
# parser internal

base/nullable.jl

+135
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,138 @@ function hash(x::Nullable, h::UInt)
215215
return hash(x.value, h + nullablehash_seed)
216216
end
217217
end
218+
219+
##############################################################################
220+
##
221+
## Standard lifting semantics
222+
##
223+
## For a function call f(xs...), return null if any x in xs is null;
224+
## otherwise, return f applied to values of xs.
225+
##
226+
##############################################################################
227+
228+
@inline function lift(f, x)
229+
if null_safe_op(f, typeof(x))
230+
return @compat Nullable(f(x.value), !isnull(x))
231+
else
232+
U = Core.Inference.return_type(f, Tuple{eltype(typeof(x))})
233+
if isnull(x)
234+
return Nullable{U}()
235+
else
236+
return Nullable(f(unsafe_get(x)))
237+
end
238+
end
239+
end
240+
241+
@inline function lift(f, x1, x2)
242+
if null_safe_op(f, typeof(x1), typeof(x2))
243+
return @compat Nullable(
244+
f(x1.value, x2.value), !(isnull(x1) | isnull(x2))
245+
)
246+
else
247+
U = Core.Inference.return_type(
248+
f, Tuple{eltype(typeof(x1)), eltype(typeof(x2))}
249+
)
250+
if isnull(x1) | isnull(x2)
251+
return Nullable{U}()
252+
else
253+
return Nullable(f(unsafe_get(x1), unsafe_get(x2)))
254+
end
255+
end
256+
end
257+
258+
@inline function lift(f, xs...)
259+
if null_safe_op(f, map(typeof, xs)...)
260+
return @compat Nullable(
261+
f(map(unsafe_get, xs)...), !(mapreduce(isnull, |, xs))
262+
)
263+
else
264+
U = Core.Inference.return_type(
265+
f, Tuple{map(x->eltype(typeof(x)), xs)...}
266+
)
267+
if hasnulls(xs)
268+
return Nullable{U}()
269+
else
270+
return Nullable(f(map(unsafe_get, xs)...))
271+
end
272+
end
273+
end
274+
275+
##############################################################################
276+
##
277+
## Non-standard lifting semantics
278+
##
279+
##############################################################################
280+
281+
# three-valued logic implementation
282+
@inline function lift(::typeof(&), x, y)::Nullable{Bool}
283+
return ifelse( isnull(x),
284+
ifelse( isnull(y),
285+
Nullable{Bool}(), # x, y null
286+
ifelse( unsafe_get(y),
287+
Nullable{Bool}(), # x null, y == true
288+
Nullable(false) # x null, y == false
289+
)
290+
),
291+
ifelse( isnull(y),
292+
ifelse( unsafe_get(x),
293+
Nullable{Bool}(), # x == true, y null
294+
Nullable(false) # x == false, y null
295+
),
296+
Nullable(unsafe_get(x) & unsafe_get(y)) # x, y not null
297+
)
298+
)
299+
end
300+
301+
# three-valued logic implementation
302+
@inline function lift(::typeof(|), x, y)::Nullable{Bool}
303+
return ifelse( isnull(x),
304+
ifelse( isnull(y),
305+
Nullable{Bool}(), # x, y null
306+
ifelse( unsafe_get(y),
307+
Nullable(true), # x null, y == true
308+
Nullable{Bool}() # x null, y == false
309+
)
310+
),
311+
ifelse( isnull(y),
312+
ifelse( unsafe_get(x),
313+
Nullable(true), # x == true, y null
314+
Nullable{Bool}() # x == false, y null
315+
),
316+
Nullable(unsafe_get(x) | unsafe_get(y)) # x, y not null
317+
)
318+
)
319+
end
320+
321+
# TODO: Decide on semantics for isequal and uncomment the following
322+
# @inline function lift(::typeof(isequal), x, y)
323+
# return ifelse( isnull(x),
324+
# ifelse( isnull(y),
325+
# true, # x, y null
326+
# false # x null, y not null
327+
# ),
328+
# ifelse( isnull(y),
329+
# false, # x not null, y null
330+
# isequal(unsafe_get(x), unsafe_get(y)) # x, y not null
331+
# )
332+
# )
333+
# end
334+
335+
@inline function lift(::typeof(isless), x, y)::Bool
336+
if null_safe_op(isless, typeof(x), typeof(y))
337+
return ifelse( isnull(x),
338+
false, # x null
339+
ifelse( isnull(y),
340+
true, # x not null, y null
341+
isless(unsafe_get(x), unsafe_get(y)) # x, y not null
342+
)
343+
)
344+
else
345+
return isnull(x) ? false :
346+
isnull(y) ? true : isless(unsafe_get(x), unsafe_get(y))
347+
end
348+
end
349+
350+
@inline lift(::typeof(isnull), x) = isnull(x)
351+
@inline lift(::typeof(get), x::Nullable) = get(x)
352+
@inline lift(::typeof(get), x::Nullable, y) = get(x, y)

test/nullable.jl

+118
Original file line numberDiff line numberDiff line change
@@ -387,3 +387,121 @@ end
387387

388388
# issue #11675
389389
@test repr(Nullable()) == "Nullable{Union{}}()"
390+
391+
##############################################################################
392+
##
393+
## Test standard lifting semantics
394+
##
395+
##############################################################################
396+
397+
types = [
398+
Float16,
399+
Float32,
400+
Float64,
401+
Int128,
402+
Int16,
403+
Int32,
404+
Int64,
405+
Int8,
406+
UInt16,
407+
UInt32,
408+
UInt64,
409+
UInt8,
410+
]
411+
412+
f(x::Number) = 5 * x
413+
f(x::Number, y::Number) = x + y
414+
f(x::Number, y::Number, z::Number) = x + y * z
415+
416+
for T in types
417+
a = one(T)
418+
x = Nullable{T}(a)
419+
y = Nullable{T}()
420+
421+
U1 = Core.Inference.return_type(f, Tuple{T})
422+
@test isequal(SQ.lift(f, x), Nullable(f(a)))
423+
@test isequal(SQ.lift(f, y), Nullable{U1}())
424+
425+
U2 = Core.Inference.return_type(f, Tuple{T, T})
426+
@test isequal(SQ.lift(f, x, x), Nullable(f(a, a)))
427+
@test isequal(SQ.lift(f, x, y), Nullable{U2}())
428+
429+
U3 = Core.Inference.return_type(f, Tuple{T, T, T})
430+
@test isequal(SQ.lift(f, x, x, x), Nullable(f(a, a, a)))
431+
@test isequal(SQ.lift(f, x, y, x), Nullable{U3}())
432+
end
433+
434+
##############################################################################
435+
##
436+
## Test non-standard lifting semantics
437+
##
438+
##############################################################################
439+
440+
# three-valued logic
441+
442+
# & truth table
443+
v1 = SQ.lift(&, Nullable(true), Nullable(true))
444+
v2 = SQ.lift(&, Nullable(true), Nullable(false))
445+
v3 = SQ.lift(&, Nullable(true), Nullable{Bool}())
446+
v4 = SQ.lift(&, Nullable(false), Nullable(true))
447+
v5 = SQ.lift(&, Nullable(false), Nullable(false))
448+
v6 = SQ.lift(&, Nullable(false), Nullable{Bool}())
449+
v7 = SQ.lift(&, Nullable{Bool}(), Nullable(true))
450+
v8 = SQ.lift(&, Nullable{Bool}(), Nullable(false))
451+
v9 = SQ.lift(&, Nullable{Bool}(), Nullable{Bool}())
452+
453+
@test isequal(v1, Nullable(true))
454+
@test isequal(v2, Nullable(false))
455+
@test isequal(v3, Nullable{Bool}())
456+
@test isequal(v4, Nullable(false))
457+
@test isequal(v5, Nullable(false))
458+
@test isequal(v6, Nullable(false))
459+
@test isequal(v7, Nullable{Bool}())
460+
@test isequal(v8, Nullable(false))
461+
@test isequal(v9, Nullable{Bool}())
462+
463+
# | truth table
464+
u1 = SQ.lift(|, Nullable(true), Nullable(true))
465+
u2 = SQ.lift(|, Nullable(true), Nullable(false))
466+
u3 = SQ.lift(|, Nullable(true), Nullable{Bool}())
467+
u4 = SQ.lift(|, Nullable(false), Nullable(true))
468+
u5 = SQ.lift(|, Nullable(false), Nullable(false))
469+
u6 = SQ.lift(|, Nullable(false), Nullable{Bool}())
470+
u7 = SQ.lift(|, Nullable{Bool}(), Nullable(true))
471+
u8 = SQ.lift(|, Nullable{Bool}(), Nullable(false))
472+
u9 = SQ.lift(|, Nullable{Bool}(), Nullable{Bool}())
473+
474+
@test isequal(u1, Nullable(true))
475+
@test isequal(u2, Nullable(true))
476+
@test isequal(u3, Nullable(true))
477+
@test isequal(u4, Nullable(true))
478+
@test isequal(u5, Nullable(false))
479+
@test isequal(u6, Nullable{Bool}())
480+
@test isequal(u7, Nullable(true))
481+
@test isequal(u8, Nullable{Bool}())
482+
@test isequal(u9, Nullable{Bool}())
483+
484+
# others
485+
486+
x1 = Nullable(1)
487+
x2 = Nullable(2)
488+
y = Nullable{Int}()
489+
z1 = 1
490+
z2 = 2
491+
492+
@test SQ.lift(isnull, x1) == false
493+
@test SQ.lift(isnull, y) == true
494+
495+
@test SQ.lift(isless, x1, y) == true
496+
@test SQ.lift(isless, y, x1) == false
497+
@test SQ.lift(isless, x1, x2) == true
498+
@test SQ.lift(isless, x2, x1) == false
499+
@test SQ.lift(isless, y, y) == false
500+
@test SQ.lift(isless, x1, z2) == true
501+
@test SQ.lift(isless, x2, z1) == false
502+
@test SQ.lift(isless, z1, x2) == true
503+
@test SQ.lift(isless, z2, x1) == false
504+
505+
@test SQ.lift(get, x1) == 1
506+
@test_throws NullException SQ.lift(get, y)
507+
@test SQ.lift(get, y, 1) == 1

0 commit comments

Comments
 (0)