Union of Nothing and Parametric Types

Hi, I am trying to define a struct that can take an Array of type T or a nothing, as recommended since Nullables has been deprecated.

using Compat
struct bar{T <: Real, M <: String}
    a::Union{Nothing,Array{T}}
    b::Array{M}
end
foo = bar(nothing, ["a", "b"])

But I get this error

MethodError: no method matching bar(::Void, ::Array{String,1})
Closest candidates are:
  bar(::Union{Array{T<:Real,N} where N, Void}, ::Array{M<:String,N} where N) where {T<:Real, M<:String} at In[37]:2

Which seems very contradictory since the error shows the declared union as possible.

The issue is that the constructor can’t figure out what T is supposed to be. All you’re giving the constructor is nothing and no T can be inferred. You can circumvent this by doing something like you probably had to do before w/ Nullable:

struct bar{T <: Real, M <: String}
    a::Union{Nothing,Array{T}}
    b::Array{M}
end
bar(::Type{T}, b::Array{M}) where {T <: Real, M <: String) = bar{T, M}(nothing, b)

This solves the problem because you’re now explicitly giving T as the 1st argument.

2 Likes

Just curious, what’s the difference between Void and Nothing?

I’ve seen people use both to achieve this Union behavior with nil values


edit: nvm. prob related to,

https://github.com/JuliaLang/julia/issues/25082

No difference between Void and Nothing, just renaming so that it makes more sense, giving that Void() is nothing, and also because Missing and missing were added.

So now, unlike most languages, we have the programmer’s null (Nothing), the 3VL null (Missing), still have the NaN value for invalid floating point data, and a zero-length string (“”) is also separate.

Which is pretty cool, because most languages mess this up.

5 Likes

Thi syntax seems to have fixed the problem

using Compat
struct bar{T <: Union{Nothing,Array{R,1}} where R <: Real, M <: String}
    a::T
    b::Array{M}
end
foo = bar(nothing, ["a", "b"])
foo2 = bar([1.0, 2.0], ["a", "b"])

The question is which performs better. From the tips mentioned in Performance Tips seems that it should be preferable to declare the union in the parametric type.

A second problem arises when I try to create an inner constructor as follows:

using Compat
struct bar{T <: Union{Nothing,Array{R}} where R <: Real, M <: String}
    a::T
    b::Array{M}
    function bar{T,M}(a,b) where {T <: Union{Nothing,Array{R}} where R <: Real, M <: String}
        if !isa(a, Nothing)
            new(a*3, b)
        else
            new(nothing, b)
        end
    end
end

I get the error

foo = bar(nothing, ["a", "b"])
ERROR: MethodError: no method matching bar(::Void, ::Array{String,1})
foo2 = bar([1.0, 2.0], ["a", "b"])
ERROR: MethodError: no method matching bar(::Array{Float64,1}, ::Array{String,1})

It is not clear to me what the right syntax is in this case.

Thanks, I will test the solution you provided here.

How about this?

struct Bar{T <: Union{Nothing, Vector{<:Real}} where {M <: AbstractString}}

(a bit shorter, also allows M to be any type of string :grinning:)

Yes, this works but still has the problem with the inner constructor, thanks.

Does it need to be an inner constructor?
I never use them unless I really need to restrict the way the type is constructed.

Yes, in my real application I need something that populates fields in the Struct as well as having some fields be nothing, sort of like the example

using Compat
struct bar{T <: Union{Nothing,Array{<:Real}} where R <: Real, M <: String}
    a::T
    b::Array{M}
    function bar{T,M}(a,b) where {T <: Union{Nothing,Array{R}} where R <: Real, M <: String}
        if !isa(a, Nothing)
            new(a*3, b)
        else
            new(nothing, b)
        end
    end
end

I understand that, I use a structure that using Nothing for 3 of the four parameterized types, when they are not needed.
I just don’t understand why you need to use an inner constructor, instead of one or two outer constructors.

The following works:

struct Bar{T <: Union{Nothing, Vector{<:Real}}, S <: AbstractString}
    a::T
    b::Vector{S}
end

Bar(::Type{Nothing}, b::Vector{S}) where {S <: AbstractString} =
    Bar{Nothing, S}(nothing, b)
Bar(a::T, b::Vector{S}) where {T <: Vector{<:Real}, S <: AbstractString} =
    Bar{T, S}(a*3, b)

foo = Bar(nothing, ["a", "b"])
println(foo)
foo2 = Bar([1.0, 2.0], ["a", "b"])
println(foo2)

If you have 3 fields in your struct but you want one to be calculated internally, is that considered bad practice?

For example code like this where the struct takes a and b but c is calculated by the inner constructor as a default and the user never inputs c.

using Compat
struct bar{T <: Union{Nothing,Array{<:Real}}, M <: String}
    a::T
    b::Array{M}
    c::Array{Float64}
    function bar{T,M}(a,b) where {T <: Union{Nothing,Array{<:Real}}, M <: String}
        if !isa(a, Nothing)
            new(a, b, a*3)
        else
            new(nothing, b, [1.0])
        end
    end
end

A broader question is when should one use inner constructors?

But, why does that need to be an inner constructor?
The only time from what I understand you should be using an inner constructor, is if you want to disallow people from creating an instance of “Bar” (it’s confusing for me to see “bar”, since the convention is to be mixed case for module and type names), without some validation of the arguments.

Otherwise, it’s a lot easier to just define a set of outer constructors.

2 Likes

Thanks, yeah probably the best approach is to define the constructors outside and also avoid a headache. Our objective was to guarantee consistency in the fields by precisely not-allowing user definition of some fields of the struct.

PS. Sorry for the bar in lower case, I just made a sloppy quick example.

Question, do you really need to pass nothing?
You could define two constructors, one with one arg, the other with two args.

2 Likes

I just tested again, using inner constructors, all worked fine:

struct Bar{T <: Union{Nothing, Vector{<:Real}}, S <: AbstractString}
    a::T
    b::Vector{S}
    c::Array{Float64}

    Bar(b::Vector{S}) where {S <: AbstractString} =
        new{Nothing, S}(nothing, b, [1.0])
    Bar(a::T, b::Vector{S}) where {T <: Vector{<:Real}, S <: AbstractString} =
        new{T, S}(a, b, a*3)
end

foo = Bar(["a", "b"])
println(foo)
foo2 = Bar([1.0, 2.0], ["a", "b"])
println(foo2)
2 Likes

Great, I had no idea it was possible to define two inner constructors in the same struct.

Thanks