Eltype of structs containing true or false values

Hey All,

I am creating a struct and a corresponding iterable. I want to make the eltype of the iterable more specific but am running into an issue since part of the eltype of my struct is true / false:

simple example:

struct MyStruct{B, T<:AbstractArray} # B here is either true or false
  x::T
  ...
end

struct MyIterator
  x::vector{MyStruct}
end

Base.eltype(::Type{MyIterator}) = MyStruct # I want to be able to pass the type of T as well.

B here denotes special cases of MyStruct that I use in specialized functions later on (e.g. f(s::MyStruct{true, T}) where T)
Problem here is then when I want to pass T along like so:

struct MyIterator{T}
  x::Vector{MyStruct}
end
Base.eltype(::Type{MyIterator{T}} where T = MyStruct{?, T} # I also need to specify ? here with a type

The problem is of course that instead of the ? we have the value true or false. I want MyIterator to handle elements
with either MyStruct{false} or MyStruct{true}.

Is there something I am missing or is this just not possible and should I instead create an abstract type;
abstract type MyAbstractStruct{T<:AbstractArray} end
and create two distinct structs for MyStruct?

Help is very much appreciated.

Please forgive me if I’m misinterpreting the question, but, in its current state, MyIterator is type unstable, and so inferring its eltype is impossible from the type alone. In other words, the type doesn’t contain enough information for eltype to return a concrete type, which in this case is what you want as MyStruct{B,A} is concrete.

If you want to write eltype like that, you’re half right in that you need to have MyIterator be two different structs, but you don’t need to define an abstract MyIterator: parameterised structs without the parameters specified are abstract.

If you write

struct MyIterator{T<:MyStruct}
  x::Vector{T}
end
Base.eltype(::Type{MyIterator{T}}) where {T} = T

then, while you haven’t constrained MyIterator to contain only MyStructs, you do know that as long as T is concrete i.e. T = MyStruct{B,A}, then it will contain both B and A.

You could instead parameterise MyIterator in the same way as MyStruct but this depends entirely on the codebase and how MyIterator will be used in it.

struct MyIterator{B, T<:AbstractArray}
  x::MyStruct{B,T}
end
Base.eltype(::Type{MyIterator{B,T}}) where {B,T} = MyStruct{B,T}
1 Like

This could be restricted as

struct MyIterator{T<:MyStruct}
    x::Vector{T}
end
1 Like

Yes, of course. Silly me

Hey Jacobus,

Thanks for you comment.

I am considering of making an AbstractMyStruct rather than an abstract version of MyIterator to replace MyStruct{false, T} and MyStruct{true, T} like so:

abstract type AbstractMyStruct end

MyStructA{T<:AbstractArray} <: AbstractMyStruct # Or should it be AbstractMyStruct{T}?
  x::T
  ...
end

MyStructB{T<:AbstractArray} <: AbstractMyStruct
  x::T
  ...
end

MyIterable{T<:AbstractMyStruct}
  x::Vector{T}
end

In my functions I can then use

function f(s::AbstractMyStruct)
  # do some stuff
  g(s)
end

g(s::MyStructA) = # do specialized stuff for MyStructA
g(s::MyStructB) = # do specialized stuff for MyStructB

Whereas in my current setup I for example have g(s::MyStruct{true, T}) = # do special stuff.
The elements of MyIterable should be able to be either a MyStructA or MyStructB. However, like this I think I have the same problem; the eltype of MyIterable is not concrete…

Alternatively I was hoping in my current setup (with MyStruct{B, T} with B being false or true) I could do

MyIterable{B<:Bool, T<:AbstractArray}
  x::Vector{MyStruct{B, T}} # This errors since B is not actually a Bool but rather literally the value true or false.
end

In my mind I want to do something like Vector{MyStruct{Union{false, true}, T}} but that doesn’t work.

I have the feeling what I want to do is not possible.
Maybe I should just make the bool a field of MyStruct like so

struct MyStruct{T<:AbstractArray}
  x::T
  is_special::Bool
end

g(s::MyStruct{T}) where T = s.is_special ? do_x() : do_y()

Depending on when doing x or y, the return type will be either an Vector or a Matrix, and I think the compiler will not like that…

Rule of thumb is that if you care about the T in AbstractMyStruct{T} and you want to use it (in this case to constrain the subtyping behaviour of MyStruct i.e. it’s only a subtype of the specific AbstractMyStruct when T matches), then put the T there. Otherwise, it’s implicitly there anyway.

You want to do value dispatch (that’s the googleable name for it). It’s completely possible, but you’re right that B<:Bool won’t work. <: means “is a subtype of”, and false is a value not a type, so there’s no hope in false being a subtype of Bool. You can, however, just elide the subtyping in the definition of MyIterable and write:

MyIterable{B, T<:AbstractArray}
    x::Vector{MyStruct{B, T}}
    MyIterable{B,T}(x) = B isa Bool ? new{B,T}(x) : error("B has to be a bool")
end

The constructor will now deal with whether the struct is valid at runtime, but this is generally not such a big issue.

This is the easy way around it. The hard way would be using Val. I don’t know how to do that properly so I won’t try, but if you google “value dispatch Julia” you’ll find some useful information.

MyIterable{B, T<:AbstractArray}
    x::Vector{MyStruct{B, T}}
    MyIterable{B,T}(x) = B isa Bool ? new{B,T}(x) : error("B has to be a bool")
end

This will not work since B in MyIterable is not a single value; x is a Vector containing MyStruct{true, T} and MyStruct{false, T}, so B in MyIterable will have to be both true and false.
I am looking into value dispatch but it seems there is quite a considerable overhead in performance when doing that…

Oh right, in that case it probably makes sense to have an AbstractMyStruct and two subtypes. Or, and you should definitely try this, what about

struct MyStruct{T}
    b::Bool
    x::T
end

For this specific case, MyStruct{T} (with T specified) is now concrete, which means MyIterable will have a concrete eltype. This may or may not be faster than the AbstractMyStruct method. It’s definitely worth trying both.

Okay, the way I go about it now is as follows:

struct MyStruct{T1<:AbstractArray,T2<:AbstractArray}
  x::T1 # This can be either a matrix or a vector
  t::T2 # This will always be a one dimensional Array
  y::T2 # Should always have the same type as t
end

struct MyIterable{T1<:AbstractArray,T2<:AbstractArray}
  s::Vector{MyStruct{T1,T2}
end

get_types(_::MyStruct{T1,T2}) where {T1,T2} = T1, T2

function MyIterable(s::Vector{MyStruct})
  T1, T2 = get_types(first(s))
  return MyIterable{T1,T2}(convert(Vector{MyStruct{T1,T2}}, s))
end

Base.eltype(::Type{MyIterable{T1,T2}}) where {T1,T2} = MyStruct{T1,T2}

I think my initial goal of having two types of MyStruct in MyIterable will just result in a non-concrete eltype; either containing MyStruct.x == Vector or MyStruct.x == Matrix.
With the above approach, all elements in MyIterable will have the same type, and the following check does not result in any allocations (whereas it resulted in ~4k allocations in my previous setup)

function f(iterable::MyIterable)
  a = 0.
  for s in iterable
    a += sum(s.x)
  end
  return a
end

What is left is making sure during construction of MyIterable that all entries have the same T1 and T2. I am hoping that having these concrete times will improve performance downstream, since now I will have to convert all entries of MyIterable to have a x::Matrix instead of Vectors and Matrices. I can now specialize my functions on T1 instead of the boolean that I used previously.

Syntax: You can just omit the unused variable like this:

get_types(::MyStruct{T1,T2})
1 Like

Thanks, that cleans it up just that little bit more :wink: