Nullable with an unknown type

Suppose I have this function,

function foo(x)
    if g(x)
        h(x)
    else
        nothing
    end
end

where x can be of many different types, and h(x) is type-stable, in the sense that given x’s type, h(x)'s type is correctly inferred by the compiler. How can I make foo type-stable? I should be able to use Nullable, but how can I refer to h(x)'s return type in the else branch?

function foo(x)
    if g(x)
        Nullable(h(x))
    else
        Nullable{T}()      # what should T be?
    end
end

I suppose I could use a generated function with Base.return_type, but that seems like overkill for such a common scenario.

See this discussion, especially the end. TL;DR: your best bet now is to rely on the type inference of the compiler (as you do above), but this may change in the future.

Thank you for the reference, that was a very useful read.

I just tried this solution on 0.6, and it’s not (inferred as) type-stable. As for generated functions, calling Base.return_types there triggers an error: code reflection cannot be used from generated functions.

However, I found an alternative, type-stable solution with broadcast:

nullify(b::Bool) = b ? Nullable{Bool}(true) : Nullable{Bool}()
function foo(x)
    h.(nullify(g(x)), x)  # the first argument to `h` is a dummy to trigger broadcasting to Null
end
# It works for arbitrary `g` and `h`, eg.:
g(x) = x < 10
h(_, x) = sin(x)

Broadcast’s handling of Nullable is done in broadcast_c. It calls Base._return_type(f, arg_types), which gets evaluated at compile-time. If I replace return_type with Base._return_type in your example, and_then becomes type-stable.

neat workaround, but possibly you should file the inference problem as an issue

It’s a little verbose, but instead consider

function and_then(f, x::Nullable)
    if isnull(x)
        S = Base.Broadcast._nullable_eltype(f, x)
        if isleaftype(S)
            Nullable{S}()
        else
            Nullable()
        end
    else
        Nullable(f(unsafe_get(x)))
    end
end

This is better for performance. Among possible inferred results Union{Nullable{Int}, Nullable{Union{}}} we prefer Nullable{Int}, so if inference can return a leaf type, then that’s what we return. Among possible inferred results Union{Nullable{Int}, Nullable{Union{}}} and Union{Nullable{Int}, Nullable{Integer}}, we prefer Union{Nullable{Int}, Nullable{Union{}}}, because this prevents type instability from propagating very far (since get can be inferred to return Int on that).

In summary:

  • Only use the result of type inference for the empty case
  • Only use the result of type inference if it is a leaf type; otherwise use Union{}

Note that this is the behavior also of broadcast: we guarantee that we never return a Nullable{T} where T is abstract. This is very important for performance.

By the way, you should not use anything that calls inference from generated functions. This includes broadcast and the implementations of and_then in this thread. There is no workaround besides avoiding the use of generating functions.

(Of course, in this particular case, just use broadcast :slight_smile:)