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