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
)
2 Likes