Apply function to tuple type parameters in a stable way

I’d like to apply a function to all type parameters of any given tuple type. I don’t know the size of the tuples, I would just like to accumulate the function over all type parameters. However, I cannot find a way to do this in an type inference-friendly way.

Here’s an example, I’m probably missing something obvious: right now I’m using the parameters attribute of a type.

myproperty(::Type) = false
myproperty(::Type{<:String}) = true
myproperty(::Type{<:Integer}) = true
myproperty(::Type{T}) where T <: Tuple = all(myproperty.(T.parameters))

myproperty(::T) where T = myproperty(T)  # for convenience

myproperty((1, 2, 3))         # true
myproperty((1, 2, "ciao"))    # true
myproperty((1, 2.7, "ciao"))  # false

However, the following fails:

using Test
@inferred myproperty((1, 2.7, "ciao"))
# ERROR: return type Bool does not match inferred return type Union{Missing, Bool}

If I check what the parameters attribute is, I understand that this is a SimpleVector:

julia> typeof((1, 2.7, "ciao")).parameters
svec(Int64, Float64, String)

Any way one could get a Tuple of the type parameters, so that what I’m trying to do above is type-stable?

Edit: even using fieldtypes as follows doesn’t seem to fix this

myproperty(::Type{T}) where T <: Tuple = all(myproperty.(fieldtypes(T)))

Hipshot from phone: Doesn’t all return missing in some cases, e.g if input is empty or missing?

1 Like

Also from phone: good call, that might be it. If so, I’ll have to guard against empty tuple types somehow.

Actually no, it seems like it returns true on empty collections.

I think what @DrChainsaw is hinting at is more if you have a missing inside the collection (not sure whether that would ever happen in your case though):

julia> all(x -> x < 10, [1, 2, missing])

Additional note that all(my property, T.parameters) is probably preferable (lazy?).

How about this? (apologies for the 2x post)

myproperty(::Type{T}) where T = false
myproperty(::Type{<:Union{String, Integer}}) = true
myproperty(::T) where T = myproperty(T)
myproperty(::Type{T}) where T <: Tuple = 
  any(ismissing, T.parameters) ? false : all(myproperty, T.parameters)::Bool
julia> using Test

julia> @inferred myproperty((1, 2.7, "ciao"))

julia> @inferred myproperty((1, 2.7, "ciao", missing))

julia> @inferred myproperty((1, 2, "ciao", missing))

julia> @inferred myproperty((1, 2, "ciao"))

That does it, thank you!

In my example missing should not be a problem, since all elements that go through myproperty are either a Type, or get turned into their own type. So in the end myproperty can only result in a Bool. In fact

myproperty(missing) == myproperty(Missing) == false

For some reason the compiler doesn’t get this, so it’s the ::Bool annotation that really solves my issues here. Maybe the additional safeguard is not needed?

(However good point @tlienart about lazily applying myproperty, thanks!)

1 Like

I think the problem may be with all then (and through it, reducedim); not sure if these functions can generally be type stable given they work on collections which can be anything. In your case, though if you know for sure that there’s no missing case, why not remove the branch and just do

myproperty(::Type{T}) where T <: Tuple = all(myproperty, T.parameters)::Bool

this will error if at runtime any of the parameter is missing but should be type stable (I think).