How to constraint a outer type of a variable in a struct to be the same, but allow the inner type to be different?

How to constraint a outer type of a variable in a struct to be the same, but allow the inner type to be different?
e.g.

struct Foo{A<:Union{<:AbstractMatrix, Cholesky}}
    a::A
    b::A
end

In this example a & b can be either an AbstractMatrix or a Cholesky. Both have to be of the same type. However, in the case of Cholesky I would like allow, that a is a Cholesky with an inner type of a Matrix and that b is a Cholesky with an inner type of a Diagonal or vice versa.

So a and b can both be either be an AbstractMatrix or a Cholesky. In the case of Cholesky a and b can differ, in the sense that the inner type is either a Diagonal or a Matrix.

How would I do that?

Interpreting your request literally, I defined Foo and its constructor like this:

# Copyright Β© 2021: Neven Sajko
#
# Licensed under the MIT license.

using LinearAlgebra

struct Foo{
        S <: Number,
        Ta <: Union{<:AbstractMatrix, <:Cholesky},
        Tb <: Union{<:AbstractMatrix, <:Cholesky},
}
        a :: Ta
        b :: Tb

        # NOTE: the inner constructor methods are not meant to be used directly,
        # use the outer constructor methods instead!

        function Foo{S, Ta, Tb}(
                a :: AbstractMatrix{S},
                b :: AbstractMatrix{S},
        ) where {
                S <: Number,
                Ta <: AbstractMatrix{S},
                Tb <: AbstractMatrix{S},
        }
                typeof(a) === Ta || error("type parameter 1 disagrees with the argument type")
                typeof(b) === Tb || error("type parameter 2 disagrees with the argument type")
                new{S, Ta, Tb}(a, b)
        end

        function Foo{S, Ta, Tb}(
                a :: Cholesky{S, A1},
                b :: Cholesky{S, A2},
        ) where {
                S <: Number,
                A1 <: AbstractMatrix{S},
                A2 <: AbstractMatrix{S},
                Ta <: Cholesky{S, A1},
                Tb <: Cholesky{S, A2},
        }
                typeof(a) === Ta || error("type parameter 1 disagrees with the argument type")
                typeof(b) === Tb || error("type parameter 2 disagrees with the argument type")
                new{S, Ta, Tb}(a, b)
        end
end

function Foo(
        a :: AbstractMatrix{S},
        b :: AbstractMatrix{S},
) where {
        S <: Number,
}
        Foo{S, typeof(a), typeof(b)}(a, b)
end

function Foo(
        a :: Cholesky{S, A1},
        b :: Cholesky{S, A2},
) where {
        S <: Number,
        A1 <: AbstractMatrix{S},
        A2 <: AbstractMatrix{S},
}
        Foo{S, Cholesky{S, A1}, Cholesky{S, A2}}(a, b)
end

Example REPL session:

               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.4 (2021-03-11)
 _/ |\__'_|_|_|\__'_|  |
|__/                   |

julia> include("zsoerenm.jl")
Foo

julia> A = [4. 12. -16.; 12. 37. -43.; -16. -43. 98.]
3Γ—3 Array{Float64,2}:
   4.0   12.0  -16.0
  12.0   37.0  -43.0
 -16.0  -43.0   98.0

julia> Foo(A, A)
Foo{Float64,Array{Float64,2},Array{Float64,2}}([4.0 12.0 -16.0; 12.0 37.0 -43.0; -16.0 -43.0 98.0], [4.0 12.0 -16.0; 12.0 37.0 -43.0; -16.0 -43.0 98.0])

julia> D = Diagonal(A)
3Γ—3 Diagonal{Float64,Array{Float64,1}}:
 4.0    β‹…     β‹…
  β‹…   37.0    β‹…
  β‹…     β‹…   98.0

julia> Foo(D, D)
Foo{Float64,Diagonal{Float64,Array{Float64,1}},Diagonal{Float64,Array{Float64,1}}}([4.0 0.0 0.0; 0.0 37.0 0.0; 0.0 0.0 98.0], [4.0 0.0 0.0; 0.0 37.0 0.0; 0.0 0.0 98.0])

julia> CA = cholesky(A)
Cholesky{Float64,Array{Float64,2}}
U factor:
3Γ—3 UpperTriangular{Float64,Array{Float64,2}}:
 2.0  6.0  -8.0
  β‹…   1.0   5.0
  β‹…    β‹…    3.0

julia> CD = cholesky(D)
Cholesky{Float64,Diagonal{Float64,Array{Float64,1}}}
U factor:
3Γ—3 Diagonal{Float64,Array{Float64,1}}:
 2.0   β‹…        β‹…
  β‹…   6.08276   β‹…
  β‹…    β‹…       9.89949

julia> Foo(CA, CD)
Foo{Float64,Cholesky{Float64,Array{Float64,2}},Cholesky{Float64,Diagonal{Float64,Array{Float64,1}}}}(Cholesky{Float64,Array{Float64,2}}([2.0 6.0 -8.0; 12.0 1.0 5.0; -16.0 -43.0 3.0], 'U', 0), Cholesky{Float64,Diagonal{Float64,Array{Float64,1}}}([2.0 0.0 0.0; 0.0 6.082762530298219 0.0; 0.0 0.0 9.899494936611665], 'U', 0))

Keep in mind that:

  1. You may have asked an overly specific question. That is, while I think that I answered your question, your question may have been the wrong question to ask.

  2. Someone who isn’t as new to Julia as I am may be able to improve on my code.

  3. Your question was actually also somewhat ambiguous regarding the Cholesky types, so my interpretation of the question may not align completely with what you thought you wanted.

3 Likes

I am not sure I understand the specs fully, but generally you do validation in the inner constructor.

Eg

using ArgCheck
struct Foo{TA,TB}
    a::TA
    b::TB
    function Foo(a::TA, b::TB) where {TA,TB}
        @argcheck (TA <: Cholesky && TB <: Cholesky) || (TA == TB && TB <: AbstractMatrix)
        new{TA,TB}(a, b)
    end
end
5 Likes