The call T(x)
is type-unstable if x
is type-unstable, requiring an additional type assertion T(x)::T
to be inferable. My understanding of the reason is T(x)
's return value isn’t required to be an instance of T
. But I can’t find a practical example where that’s the case, can someone help me out?
Nitpick: an object like your x
is never type unstable - type instability is always about a function. (or a constructor, such as in your example, which is “just” a special kind of function).
A practical example is if you’re building a Computer Algebra System (CAS). You might have a type Not
, encapsulating a formula:
abstract type Formula end
struct Not <: Formula
f::Formula
end
Chaining lots of Not
together like Not(Not(Not(...)))
is quite a lot of unnecessary nesting, so what you could do is add a special constructor method that “skips” a negation:
struct Not <: Formula
f::Formula
Not(f::Formula) = new(f)
Not(n::Not) = n.f
end
(Do note that this is just an example - it’s unlikely that a CAS represented as individual julia types is going to be particularly performant.)
It’s not that adding T(x)::T
makes a function call type stable, it just inserts an additional convert(T, T(X))
step that ensures the result of the conversion is assumed to be a T
.
It’s generally considered to be bad style to do this. I’m sure there are examples of it, but I think at least Base
and the stdlib try to avoid it. The easiest way to avoid this is to have a function t
which is basically a smarter version of the constructor T
.
For instance, just like @Sukera’s Not
example, base has Adjoint
and it’s usually an assumed property of well behaved vector spaces that the adjoint of an adjoint of a vector gives the original vector. Base provides an adjoint
function which as aware of this, but the Adjoint
constructor should always do additional wrapping:
julia> v = [1, 2, 3];
julia> adjoint(adjoint(v))
3-element Vector{Int64}:
1
2
3
julia> Adjoint(Adjoint(v))
3×1 adjoint(adjoint(::Vector{Int64})) with eltype Int64:
1
2
3
I think the key is that T(x)
is just a function call. And the language itself doesn’t have the ability to describe that all methods of T
return the same thing, whether T
is a type or just a plain old function.
So it’s not that T(x)
should be allowed to return something other than T
(or even that there might exist examples of such monstrosities), but rather that the language itself doesn’t know this and therefore it cannot enforce this constraint. That’s why you need to do it with the explicit ::T
.
There are cases in Base where things like T(x)::T
do happen - they mostly crop up around guaranteeing a correct conversion (or an error) when T
can be user defined as well.
typeassert(T(x), T)
, actually. The T(x)
part does similar work as convert
, and explicit convert(T, x)
calls are type-unstable if x
is type-unstable uninferable.
I think Mason is saying that T(x)
not returning something::T
is the bad style, not that T(x)::T
is bad style.
See also Require constructors and `convert` to return objects of stated type? · Issue #42372 · JuliaLang/julia · GitHub (which has a backlink that shows that Quantity(x, NoUnits)
is one counterexample… but that’s still not quite as bad as a one-arg T(x)
breaking this). And while we don’t have an open issue tracking this anymore, the ability to specify return types of entire generic functions was something that was brought up in return type declarations · Issue #1090 · JuliaLang/julia · GitHub (which was closed by implementing per-method return types). That may be worth resurrecting.
Ah yeah I wasn’t so clear there. I meant that it’s considered bad style to write a type constructor that doesn’t return an instance of that type – not it’s bad style to use a type assert.
Edit: I didn’t originally realize @mbauman beat me to the punch on this one, sorry for the noise