Hi, I am trying to understand the usage of the Some{T}:
function test(x::Union{Some{Float64}, Nothing})
if isnothing(x)
println("Nothing!")
else
println("Something!")
end
end
test(nothing) # prints "Nothing!"
test(Some(3.4)) # prints "Something!"
test(Some(nothing)) # MethodError: no method matching test(::Some{Nothing})
Given the behaviour of the last result (which I guess makes sense), I don’t understand how this is any better/different from just using Union{Float64, Nothing}.
Likely, I’m misunderstanding, or misusing how Some is expected to work.
In this example, there isn’t really a benefit of Some{Float64} over Float64. However, it is useful in general to distinguish between Some{Nothing} (the value nothing) and Nothing (the absence of a value).
In this example, say you want to get a value from a Dict{Int, Any}.
There are two ways to do this. You could index the collection directly, but you might run into an error. So, you’d have to wrap it in a try-catch block.
julia> collection[4]
ERROR: KeyError: key 4 not found
This is undesirable, so let’s try the other way: passing a default value.
julia> get(collection, 3, nothing)
In this scenario, there’s no way to distinguish between the key-value pair 3 => nothing and the failure to find the key 3, and returning nothing. You’d need to use either the try-catch block or create an entirely new sentinel value that the collection is not allowed to contain. The use of Union{Some{T}, Nothing} would have prevented this issue.
As another classic example, consider a function that finds the first first element in an array that fulfills some condition, and returns that element. If no such element is found, the function returns nothing instead.
Now, what happens when you pass isnothing as the predicate, and give an Vector{Union{Nothing, Float64}} as input? The function will return nothing but that’s a problem - usually this would signal that no element fulfilled the predicate, but in this case, nothingdoes fulfill it. If the function would return the element wrapped in Some, the distinction would be clear. You’d receive a Some(nothing) instead, and you’d know that the value that you were looking for is exactly nothing (and not that there was no value fulfilling the predicate).
Of course, this is just an example. Similar situations can come up with more complicated predicates.
It’s really worth pointing out that nothing is almost never intended to be a working input, and this is done by not writing methods at all. Say you want to find an even number, that’s simple:
and when nothing does rarely work, it’s in functions designed just to ignore it:
julia> something(nothing, nothing, 3)
3
It’s precisely because nothing is used to mean a result did not exist (I never liked the phrasing “absence of a value” because nothing is technically a value of a type), and you handle nonexistent results differently (and usually immediately) from meaningful results. If we need to handle the technical value of nothing as metadata, then as already suggested, the easy way to isolate it from the “nonexistent result”-nothing that functions can generate is to wrap it or everything in Some:
julia> Some.(findfirst.(isodd, [[2], [1], []])) # nothing as metadata
3-element Vector{Some}:
Some(nothing)
Some(1)
Some(nothing)
julia> [i for i in findfirst.(isodd, [[2], [1], []]) if !isnothing(i)] # handle nothing
1-element Vector{Int64}:
1
It’s worth contrasting this with missing, which does have methods written to propagate it and has support for direct elements in containers like skipmissing and Missings.jl. Missing data exists, so it has a different meaning from nonexistent results. R makes a similar distinction between NA (“not available”) and NULL, and it doesn’t even let the nonexistent NULL be stored in base containers. As that blog hints, you might run into inconsistencies or deviations from the standards (yes there’s more than one); for example, missing/NA might be used for nonexistent results because the propagating and skipping is convenient for processing, usually justified when missing data isn’t technically possible.
Thanks for the responses. I understand the conceptual difference between Nothing and Some{Nothing} (though I am wondering how Missing fits into all of this), but was wondering how this is used in practice.
I’ve seen Union{Nothing, Some{T}} specified as a pattern, but it doesn’t seem to work as expected in my example, unless T is left unspecified; otherwise it seems that I would need to use Union{Nothing, Some{Nothing}, Some{Float64}}, but then why Some{Float64} instead of just Float64?
I imagine Union{Nothing, Some{T}} is supposed to be a field or element type, which wraps the nothing the same way Some(nothing) does. The Some{T} part is likely intended to potentially distinguish nonexistence-nothings from metadata-nothings via Union{Nothing, Some{Nothing}}. If metadata-nothings are not necessarily relevant, e.g. T<:Number, then you’re right that the typical pattern is Union{Nothing, T}; I see that in the wild all the time, but I very rarely see Some at all. That still allows T=Some{U} when you need it, but U is not a parameter that can be shared with the other field types.