diff --git a/base/exports.jl b/base/exports.jl index 093583b5620c3..a1e193288e0de 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -61,6 +61,7 @@ export MathConst, Matrix, MergeSort, + Nullable, ObjectIdDict, OrdinalRange, PollingFileWatcher, @@ -145,6 +146,7 @@ export KeyError, LoadError, MethodError, + NullException, ParseError, ProcessExitedException, SystemError, @@ -197,7 +199,7 @@ export ≠, !==, ≡, - ≢, + ≢, $, %, &, @@ -964,7 +966,7 @@ export rfft, xcorr, -# numerical integration +# numerical integration quadgk, # iteration @@ -1010,7 +1012,7 @@ export toc, toq, -#dates +# dates Date, DateTime, now, @@ -1229,7 +1231,7 @@ export # shared arrays sdata, indexpids, - + # paths and file names abspath, basename, @@ -1323,6 +1325,9 @@ export unsafe_pointer_to_objref, unsafe_store!, +# nullable types + isnull, + # Macros @__FILE__, @b_str, diff --git a/base/nullable.jl b/base/nullable.jl new file mode 100644 index 0000000000000..40e38f20fb545 --- /dev/null +++ b/base/nullable.jl @@ -0,0 +1,50 @@ +immutable Nullable{T} + isnull::Bool + value::T + + Nullable() = new(true) + Nullable(value::T) = new(false, value) +end + +immutable NullException <: Exception +end + +Nullable{T}(value::T) = Nullable{T}(value) + +function convert{S, T}(::Type{Nullable{T}}, x::Nullable{S}) + return isnull(x) ? Nullable{T}() : Nullable(convert(T, get(x))) +end + +function show{T}(io::IO, x::Nullable{T}) + if x.isnull + @printf(io, "Nullable{%s}()", repr(T)) + else + @printf(io, "Nullable(%s)", repr(x.value)) + end +end + +get(x::Nullable) = x.isnull ? throw(NullException()) : x.value + +get{S, T}(x::Nullable{S}, y::T) = x.isnull ? convert(S, y) : x.value + +isnull(x::Nullable) = x.isnull + +function isequal{S, T}(x::Nullable{S}, y::Nullable{T}) + if x.isnull && y.isnull + return true + elseif x.isnull || y.isnull + return false + else + return isequal(x.value, y.value) + end +end + +=={S, T}(x::Nullable{S}, y::Nullable{T}) = throw(NullException()) + +function hash(x::Nullable, h::Uint) + if x.isnull + return h + uint(0x932e0143e51d0171) + else + return hash(x.value, h + uint(0x932e0143e51d0171)) + end +end diff --git a/base/sysimg.jl b/base/sysimg.jl index 3c69dbc01fea1..2c96e8ac5d835 100644 --- a/base/sysimg.jl +++ b/base/sysimg.jl @@ -270,6 +270,9 @@ importall .Profile include("Dates.jl") import .Dates: Date, DateTime, now +# nullable types +include("nullable.jl") + function __init__() # Base library init reinit_stdio() diff --git a/doc/manual/index.rst b/doc/manual/index.rst index 389b8b8eea3ba..7143b505296ad 100644 --- a/doc/manual/index.rst +++ b/doc/manual/index.rst @@ -28,6 +28,7 @@ linear-algebra networking-and-streams parallel-computing + nullable-types interacting-with-julia running-external-programs calling-c-and-fortran-code diff --git a/doc/manual/nullable-types.rst b/doc/manual/nullable-types.rst new file mode 100644 index 0000000000000..fb8efcab97401 --- /dev/null +++ b/doc/manual/nullable-types.rst @@ -0,0 +1,87 @@ +.. _man-nullable-types: + +******************************************* +Nullable Types: Representing Missing Values +******************************************* + +In many settings, you need to interact with a value of type ``T`` that may or +may not exist. To handle these settings, Julia provides a parametric type +called ``Nullable{T}``, which can be thought of as a specialized container +type that can contain either zero or one values. ``Nullable{T}`` provides a +minimal interface designed to ensure that interactions with missing values +are safe. At present, the interface consists of four possible interactions: + +- Construct a ``Nullable`` object. +- Check if an ``Nullable`` object has a missing value. +- Access the value of a ``Nullable`` object with a guarantee that a + ``NullException`` will be thrown if the object's value is missing. +- Access the value of a ``Nullable`` object with a guarantee that a default + value of type ``T`` will be returned if the object's value is missing. + +Constructing ``Nullable`` objects +--------------------------------- + +To construct an object representing a missing value of type ``T``, use the +``Nullable{T}()`` function: + +.. doctest:: + + x1 = Nullable{Int}() + x2 = Nullable{Float64}() + x3 = Nullable{Vector{Int}}() + +To construct an object representing a non-missing value of type ``T``, use the +``Nullable(x::T)`` function: + +.. doctest:: + + x1 = Nullable(1) + x2 = Nullable(1.0) + x3 = Nullable([1, 2, 3]) + +Note the core distinction between these two ways of constructing a ``Nullable`` +object: in one style, you provide a type, ``T``, as a function parameter; in +the other style, you provide a single value of type ``T`` as an argument. + +Checking if an ``Nullable`` object has a value +---------------------------------------------- + +You can check if a ``Nullable`` object has any value using the ``isnull`` +function: + +.. doctest:: + + isnull(Nullable{Float64}()) + isnull(Nullable(0.0)) + +Safely accessing the value of an ``Nullable`` object +---------------------------------------------------- + +You can safely access the value of an ``Nullable`` object using the ``get`` +function: + +.. doctest:: + + get(Nullable{Float64}()) + get(Nullable(1.0)) + +If the value is not present, as it would be for ``Nullable{Float64}``, a +``NullException`` error will be thrown. The error-throwing nature of the +``get`` function ensures that any attempt to access a missing value immediately +fails. + +In cases for which a reasonable default value exists that could be used +when a ``Nullable`` object's value turns out to be missing, you can provide this +default value as a second argument to ``get``: + +.. doctest:: + + get(Nullable{Float64}(), 0) + get(Nullable(1.0), 0) + +Note that this default value will automatically be converted to the type of +the ``Nullable`` object that you attempt to access using the ``get`` function. +For example, in the code shown above the value ``0`` would be automatically +converted to a ``Float64`` value before being returned. The presence of default +replacement values makes it easy to use the ``get`` function to write +type-stable code that interacts with sources of potentially missing values. diff --git a/test/nullable.jl b/test/nullable.jl new file mode 100644 index 0000000000000..b46a26c339c5d --- /dev/null +++ b/test/nullable.jl @@ -0,0 +1,230 @@ +types = [ + Bool, + Char, + Float16, + Float32, + Float64, + Int128, + Int16, + Int32, + Int64, + Int8, + Uint16, + Uint32, + Uint64, + Uint8, +] + +# Nullable{T}() = new(true) +for T in types + x = Nullable{T}() + @test x.isnull === true + @test isa(x.value, T) +end + +# Nullable{T}(value::T) = new(false, value) +for T in types + x = Nullable{T}(zero(T)) + @test x.isnull === false + @test isa(x.value, T) + @test x.value === zero(T) + + x = Nullable{T}(one(T)) + @test x.isnull === false + @test isa(x.value, T) + @test x.value === one(T) +end + +# immutable NullException <: Exception +@test isa(NullException(), NullException) +@test_throws NullException throw(NullException()) + +# Nullable{T}(value::T) = Nullable{T}(value) +for T in types + v = zero(T) + x = Nullable(v) + @test x.isnull === false + @test isa(x.value, T) + @test x.value === v + + v = one(T) + x = Nullable(v) + @test x.isnull === false + @test isa(x.value, T) + @test x.value === v +end + +p1s = [ + "Nullable{Bool}()", + "Nullable{Char}()", + "Nullable{Float16}()", + "Nullable{Float32}()", + "Nullable{Float64}()", + "Nullable{Int128}()", + "Nullable{Int16}()", + "Nullable{Int32}()", + "Nullable{Int64}()", + "Nullable{Int8}()", + "Nullable{Uint16}()", + "Nullable{Uint32}()", + "Nullable{Uint64}()", + "Nullable{Uint8}()", +] + +p2s = [ + "Nullable(false)", + "Nullable('\0')", + "Nullable(float16(0.0))", + "Nullable(0.0f0)", + "Nullable(0.0)", + "Nullable(0)", + "Nullable(0)", + "Nullable(0)", + "Nullable(0)", + "Nullable(0)", + "Nullable(0x0000)", + "Nullable(0x00000000)", + "Nullable(0x0000000000000000)", + "Nullable(0x00)", +] + +p3s = [ + "Nullable(true)", + "Nullable('\x01')", + "Nullable(float16(1.0))", + "Nullable(1.0f0)", + "Nullable(1.0)", + "Nullable(1)", + "Nullable(1)", + "Nullable(1)", + "Nullable(1)", + "Nullable(1)", + "Nullable(0x0001)", + "Nullable(0x00000001)", + "Nullable(0x0000000000000001)", + "Nullable(0x01)", +] + +# show{T}(io::IO, x::Nullable{T}) +io = IOBuffer() +for (i, T) in enumerate(types) + x1 = Nullable{T}() + x2 = Nullable(zero(T)) + x3 = Nullable(one(T)) + show(io, x1) + takebuf_string(io) == p1s[i] + show(io, x2) + takebuf_string(io) == p2s[i] + show(io, x3) + takebuf_string(io) == p3s[i] +end + +# get(x::Nullable) +for T in types + x1 = Nullable{T}() + x2 = Nullable(zero(T)) + x3 = Nullable(one(T)) + + @test_throws NullException get(x1) + @test get(x2) === zero(T) + @test get(x3) === one(T) +end + +# get{S, T}(x::Nullable{S}, y::T) +for T in types + x1 = Nullable{T}() + x2 = Nullable(zero(T)) + x3 = Nullable(one(T)) + + @test get(x1, zero(T)) === zero(T) + @test get(x1, one(T)) === one(T) + @test get(x2, one(T)) === zero(T) + @test get(x3, zero(T)) === one(T) +end + +# isnull(x::Nullable) +for T in types + x1 = Nullable{T}() + x2 = Nullable(zero(T)) + x3 = Nullable(one(T)) + + @test isnull(x1) === true + @test isnull(x2) === false + @test isnull(x3) === false +end + +# function isequal{S, T}(x::Nullable{S}, y::Nullable{T}) +for T in types + x1 = Nullable{T}() + x2 = Nullable{T}() + x3 = Nullable(zero(T)) + x4 = Nullable(one(T)) + + @test isequal(x1, x1) === true + @test isequal(x1, x2) === true + @test isequal(x1, x3) === false + @test isequal(x1, x4) === false + + @test isequal(x2, x1) === true + @test isequal(x2, x2) === true + @test isequal(x2, x3) === false + @test isequal(x2, x4) === false + + @test isequal(x3, x1) === false + @test isequal(x3, x2) === false + @test isequal(x3, x3) === true + @test isequal(x3, x4) === false + + @test isequal(x4, x1) === false + @test isequal(x4, x2) === false + @test isequal(x4, x3) === false + @test isequal(x4, x4) === true +end + +# function =={S, T}(x::Nullable{S}, y::Nullable{T}) +for T in types + x1 = Nullable{T}() + x2 = Nullable{T}() + x3 = Nullable(zero(T)) + x4 = Nullable(one(T)) + + @test_throws NullException (x1 == x1) + @test_throws NullException (x1 == x2) + @test_throws NullException (x1 == x3) + @test_throws NullException (x1 == x4) + + @test_throws NullException (x2 == x1) + @test_throws NullException (x2 == x2) + @test_throws NullException (x2 == x3) + @test_throws NullException (x2 == x4) + + @test_throws NullException (x3 == x1) + @test_throws NullException (x3 == x2) + @test_throws NullException (x3 == x3) + @test_throws NullException (x3 == x4) + + @test_throws NullException (x4 == x1) + @test_throws NullException (x4 == x2) + @test_throws NullException (x4 == x3) + @test_throws NullException (x4 == x4) +end + +# function hash(x::Nullable, h::Uint) +for T in types + x1 = Nullable{T}() + x2 = Nullable{T}() + x3 = Nullable(zero(T)) + x4 = Nullable(one(T)) + + @test isa(hash(x1), Uint) + @test isa(hash(x2), Uint) + @test isa(hash(x3), Uint) + @test isa(hash(x4), Uint) + + @test hash(x1) == hash(x2) + @test hash(x1) != hash(x3) + @test hash(x1) != hash(x4) + @test hash(x2) != hash(x3) + @test hash(x2) != hash(x4) + @test hash(x3) != hash(x4) +end diff --git a/test/runtests.jl b/test/runtests.jl index 49798ce055db7..b4a901af28861 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,7 +9,7 @@ testnames = [ "floatapprox", "readdlm", "reflection", "regex", "float16", "combinatorics", "sysinfo", "rounding", "ranges", "mod2pi", "euler", "show", "lineedit", "replcompletions", "repl", "test", "examples", "goto", - "llvmcall", "grisu" + "llvmcall", "grisu", "nullable" ] @unix_only push!(testnames, "unicode") @@ -35,7 +35,7 @@ end cd(dirname(@__FILE__)) do n = 1 - if net_on + if net_on n = min(8, CPU_CORES, length(tests)) n > 1 && addprocs(n; exeflags=`--check-bounds=yes`) blas_set_num_threads(1)