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.

1 Like

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:)

2 Likes