Skip to content
This repository was archived by the owner on May 4, 2019. It is now read-only.

Commit c8a4bd9

Browse files
committed
experimental functional operations on nullables as collections
1 parent 9842b91 commit c8a4bd9

File tree

4 files changed

+245
-2
lines changed

4 files changed

+245
-2
lines changed

src/NullableArrays.jl

+5
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ include("reduce.jl")
3030
include("show.jl")
3131
include("subarray.jl")
3232

33+
if VERSION v"0.5-"
34+
include("functional.jl")
35+
using .FunctionalNullableOperations
36+
end
37+
3338
end

src/functional.jl

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
module FunctionalNullableOperations
2+
3+
# extends Base with operations that treat nullables as collections
4+
5+
importall Base
6+
import Base: promote_op, LinearFast
7+
8+
# conceptually, nullable types can be considered infinite-order tensor powers of
9+
# finite-dimensional vector spaces — we attempt to support most operations on
10+
# arrays except the linear algebra related ones; the infinite-dimensional nature
11+
# makes subtyping AbstractArray a little dangerous, which explains why
12+
# functionality is reimplemented instead of subtyping AbstractArray
13+
14+
# size – not implemented since an infinite-dimensional tuple would be strange
15+
# length – one or zero
16+
# endof – same as length
17+
length(u::Nullable) = u.isnull ? 0 : 1
18+
endof(u::Nullable) = length(u)
19+
20+
# indexing is either without index, or with 1 as index
21+
# generalized linear indexing is not supported
22+
# setindex! not supported because Nullable is immutable
23+
linearindexing{T}(::Nullable{T}) = LinearFast()
24+
function getindex(u::Nullable)
25+
@boundscheck u.isnull && throw(NullException())
26+
u.value
27+
end
28+
function getindex(u::Nullable, i::Integer)
29+
@boundscheck u.isnull | (i one(i)) && throw(BoundsError(i, u))
30+
u.value
31+
end
32+
33+
# iteration protocol
34+
start(u::Nullable) = 1
35+
next(u::Nullable, i::Integer) = u.value, 0
36+
done(u::Nullable, i::Integer) = u.isnull || i == 0
37+
38+
# next we have reimplementations of some higher-order functions
39+
filter{T}(p, u::Nullable{T}) = u.isnull ? u : p(u.value) ? u : Nullable{T}()
40+
41+
# warning: not type-stable
42+
map{T}(f, u::Nullable{T}) = u.isnull ? Nullable{Union{}}() : Nullable(f(u.value))
43+
44+
# multi-argument map doesn't broadcast, so not very useful, but no harm having
45+
# it...
46+
function map(f, us::Nullable...)
47+
if all(isnull, us)
48+
Nullable()
49+
elseif !any(isnull, us)
50+
Nullable(map(f, map(getindex, us)...))
51+
else
52+
throw(DimensionMismatch("expected all null or all nonnull"))
53+
end
54+
end
55+
56+
# foldr and foldl are quite useful to express "do something if not null, else"
57+
# these
58+
59+
# being infinite-dimensional, nullables are generally incompatible with
60+
# broadcast with arrays — it is probably not worth supporting the rare case
61+
# where an array has length 0 or 1
62+
63+
# so we reimplement broadcast, which has the same semantics but very different
64+
# implementation. This implementation is in fact much simpler than that for
65+
# arrays. Length-1 (non-nulls) are flexible and can broadcast to length-0
66+
# (nulls), but the other way does not work. Numbers are zero-dimensional and can
67+
# broadcast to infinite-dimensional nullables, but the other direction is not
68+
# supported.
69+
70+
# there are two shapes we are concerned about: infinite-dimensional 1×1×...
71+
# and infinite-dimensional 0×0×...; we don't care about zero-dimensional because
72+
# in that case all arguments were numbers, and broadcasting over only numbers
73+
# isn't supported by base currently
74+
function nullable_broadcast_shape(us::Union{Nullable,Number}...)
75+
for u in us
76+
if isa(us, Nullable)
77+
if u.isnull
78+
return true
79+
end
80+
end
81+
end
82+
return false
83+
end
84+
85+
# Base's broadcast has a very loose signature so we can easily make it more
86+
# specific. Broadcast on numbers is still not supported. FIXME: remove generated
87+
# functions where unnecessary
88+
89+
# some specialized functions
90+
broadcast{T}(f, u::Nullable{T}) =
91+
u.isnull ? Nullable{promote_op(f, T)}() : Nullable{promote_op(f, T)}(f(u.value))
92+
93+
@generated function broadcast(f, u::Union{Nullable,Number}, v::Union{Nullable,Number})
94+
checkfor(s) = :($s.isnull && return Nullable{result}())
95+
lifted = [T <: Nullable ? T.parameters[1] : T for T in (u, v)]
96+
checks = vcat(u <: Nullable ? [checkfor(:u)] : [],
97+
v <: Nullable ? [checkfor(:v)] : [])
98+
quote
99+
result = promote_op(f, $(lifted...))
100+
$(checks...)
101+
@inbounds return Nullable{result}(f(u[], v[]))
102+
end
103+
end
104+
105+
# functions with three arguments or more are a bit expensive to specialize...
106+
# FIXME: why the arbitrary cutoff? justify
107+
function broadcast(f, us::Union{Nullable, Number}...)
108+
result = promote_op(f,
109+
[T <: Nullable ? T.parameters[1] : T for T in map(typeof, us)]...)
110+
for u in us
111+
if isa(u, Nullable) && u.isnull
112+
return Nullable{result}()
113+
end
114+
end
115+
@inbounds return Nullable{result}(f(map(getindex, us)...))
116+
end
117+
118+
# FIXME: these operations are probably not all correct
119+
# and definitely some of them are slow, needs specialization
120+
# also have to be careful to avoid ambiguities... needs testing
121+
for eop in :(.+, .-, .*, ./, .\, .//, .==, .<, .!=, .<=, , .%, .<<, .>>, .^).args
122+
@eval $eop(u::Nullable, v::Union{Nullable, Number}) = broadcast($eop, u, v)
123+
@eval $eop(u::Number, v::Nullable) = broadcast($eop, u, v)
124+
end
125+
126+
end # module

test/functional.jl

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
module TestFunctional
2+
3+
# nullable equality is a little strange... we need a "is a null with type T"
4+
# and a "is a nullable with value egal to k"
5+
6+
# these functions are curried for readability
7+
isnull_typed(u::Nullable, T::Type) = typeof(u).parameters[1] == T && isnull(u)
8+
isnull_typed(t::Type) = u -> isnull_typed(u, t)
9+
10+
isnullableof(u::Nullable, x) = !isnull(u) && u.value === x
11+
isnullableof(x) = u -> isnullableof(u, x)
12+
13+
using Base.Test
14+
15+
# getindex
16+
@test_throws NullException Nullable()[]
17+
@test_throws NullException Nullable{Int}()[]
18+
@test Nullable(0)[] == 0
19+
20+
@test_throws BoundsError Nullable()[1]
21+
@test_throws BoundsError Nullable(0)[0]
22+
@test_throws BoundsError Nullable(0)[2]
23+
@test Nullable(0)[1] == 0
24+
25+
# collect
26+
@test collect(Nullable()) == Union{}[]
27+
@test collect(Nullable{Int}()) == Int[]
28+
@test collect(Nullable(85)) == Int[85]
29+
@test collect(Nullable(1.0)) == Float64[1.0]
30+
31+
# length
32+
@test length(Nullable()) == 0
33+
@test length(Nullable{Int}()) == 0
34+
@test length(Nullable(1.0)) == 1
35+
@test endof(Nullable(85)) == 1
36+
37+
# filter
38+
for p in (_ -> true, _ -> false)
39+
@test filter(p, Nullable()) |> isnull_typed(Union{})
40+
@test filter(p, Nullable{Int}()) |> isnull_typed(Int)
41+
end
42+
@test filter(_ -> true, Nullable(85)) |> isnullableof(85)
43+
@test filter(_ -> false, Nullable(85)) |> isnull_typed(Int)
44+
@test filter(x -> x > 0, Nullable(85)) |> isnullableof(85)
45+
@test filter(x -> x < 0, Nullable(85)) |> isnull_typed(Int)
46+
47+
# map
48+
sqr(x) = x^2
49+
@test map(sqr, Nullable()) |> isnull_typed(Union{})
50+
@test map(sqr, Nullable{Int}()) |> isnull_typed(Union{}) # type-unstable (!)
51+
@test map(sqr, Nullable(2)) |> isnullableof(4)
52+
53+
@test map(+, Nullable(1), Nullable(2), Nullable(3)) |> isnullableof(6)
54+
@test map(+, Nullable(), Nullable(), Nullable()) |> isnull_typed(Union{})
55+
@test map(+, Nullable{Int}(), Nullable{Int}()) |> isnull_typed(Union{})
56+
57+
# example: square if value exists, -1 if value is null
58+
# with foldl/foldr/reduce
59+
for fn in (foldl, foldr, reduce)
60+
@test foldl((_, x) -> x^2, -1, Nullable()) == -1
61+
@test foldl((_, x) -> x^2, -1, Nullable(10)) == 100
62+
end
63+
64+
# with broadcast and get (map does not work because of get limitations...)
65+
# perhaps the get limitations should be fixed
66+
@test get(sqr.(Nullable{Int}()), -1) == -1
67+
@test get(sqr.(Nullable(10)), -1) == 100
68+
69+
# broadcast and elementwise
70+
@test sin.(Nullable(0.0)) |> isnullableof(0.0)
71+
@test sin.(Nullable{Float64}()) |> isnull_typed(Float64)
72+
73+
@test Nullable(8) .+ Nullable(10) |> isnullableof(18)
74+
@test Nullable(8) .- Nullable(10) |> isnullableof(-2)
75+
@test Nullable(8) .+ Nullable{Int}() |> isnull_typed(Int)
76+
@test Nullable{Int}() .- Nullable(10) |> isnull_typed(Int)
77+
78+
@test log.(10, Nullable(1.0)) |> isnullableof(0.0)
79+
@test log.(10, Nullable{Float64}()) |> isnull_typed(Float64)
80+
81+
@test Nullable(2) .^ Nullable(4) |> isnullableof(16)
82+
@test Nullable(2) .^ Nullable{Int}() |> isnull_typed(Int)
83+
84+
# big broadcast (slow)
85+
@test Nullable(1) .+ Nullable(1) .+ Nullable(1) .+ Nullable(1) .+ Nullable(1) .+
86+
Nullable(1) |> isnullableof(6)
87+
@test Nullable(1) .+ Nullable(1) .+ Nullable(1) .+ Nullable{Int}() .+
88+
Nullable(1) .+ Nullable(1) |> isnull_typed(Int)
89+
90+
# very slow but it should work
91+
us = map(Nullable, 1:20)
92+
@test broadcast(max, us...) |> isnullableof(20)
93+
@test broadcast(max, us..., Nullable{Int}()) |> isnull_typed(Int)
94+
95+
# imperative style
96+
s = 0
97+
for x in Nullable(10)
98+
s += x
99+
end
100+
@test s == 10
101+
102+
s = 0
103+
for x in Nullable{Int}()
104+
s += x
105+
end
106+
@test s == 0
107+
108+
end

test/runtests.jl

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ fatalerrors = length(ARGS) > 0 && ARGS[1] == "-f"
44
quiet = length(ARGS) > 0 && ARGS[1] == "-q"
55
anyerrors = false
66

7-
my_tests = [
7+
my_tests = [] #= [
88
"typedefs.jl",
99
"constructors.jl",
1010
"primitives.jl",
@@ -17,7 +17,11 @@ my_tests = [
1717
"operators.jl",
1818
"subarray.jl",
1919
"show.jl",
20-
]
20+
] =#
21+
22+
if VERSION v"0.5-"
23+
push!(my_tests, "functional.jl")
24+
end
2125

2226
println("Running tests:")
2327

0 commit comments

Comments
 (0)