Constructor Confusion

I’m trying to write code for a new statistical distribution, the Johnson distribution family, for the Distributions.jl package.

I need to create a structure for the Johnson distribution and constructors for it. I’ve generally been copying the code for symtriangular.jl (the symmetric triangular distribution).

The Johnson distribution is actually a family of 3 distributions, called “SL”, “SB”, and “SU”. Each of those has 4 parameters – γ,δ,ξ,λ So the Johnson distribution has 5 parameters:

JohnsonDist(member,γ,δ,ξ,λ)

Where “member” is a string (AbstractString), and the rest are Reals.

Here is the struct I have so far for the Johnson distribution family:

# Code for Johnson Distribution 
struct JohnsonDist{T0<:AbstractString, T1<:Real, T2<:Real, T3<:Real, T4<:Real} <: ContinuousUnivariateDistribution
    member::T0
    γ::T1
    δ::T2
    ξ::T3
    λ::T4

    JohnsonDist(member::T0, γ::T1, δ::T2, ξ::T3, λ::T4) where {T0<:AbstractString, T1<:Real, T2<:Real, T3<:Real, T4<:Real} = new{T0, T1, T2, T3, T4}(uppercase(member), γ, δ, ξ, λ)
end

If I execute this, it works well to create structs

julia> myj1=JohnsonDist("SB",0.5,1.0,1.0,1.0)
JohnsonDist{String, Float64, Float64, Float64, Float64}(
member: SB
γ: 0.5
δ: 1.0
ξ: 1.0
λ: 1.0
)

However, it would be nice to build some constraints on the parameters – for each of SL, SB, SL, there are different constraints on γ,δ,ξ,λ.

Let me pause the story here and present the relevant code for the Symmetric Triangular distribution, from symtriangle.jl:

# from utils.jl in Distributions package 
macro check_args(D, cond)
    quote
        if !($(esc(cond)))
            throw(ArgumentError(string(
                $(string(D)), ": the condition ", $(string(cond)), " is not satisfied.")))
        end
    end
end

# code from Symmetric Triangular Distribution 
struct SymTriangularDist{T<:Real} <: ContinuousUnivariateDistribution
    μ::T
    σ::T
    SymTriangularDist{T}(µ::T, σ::T) where {T <: Real} = new{T}(µ, σ)
end

function SymTriangularDist(μ::T, σ::T; check_args=true) where {T <: Real}
    check_args && @check_args(SymTriangularDist, σ > zero(σ))
    return SymTriangularDist{T}(μ, σ)
end

SymTriangularDist(μ::Real, σ::Real) = SymTriangularDist(promote(μ, σ)...)
SymTriangularDist(μ::Integer, σ::Integer) = SymTriangularDist(float(μ), float(σ))
SymTriangularDist(μ::T) where {T <: Real} = SymTriangularDist(μ, one(T))
SymTriangularDist() = SymTriangularDist(0.0, 1.0, check_args=false)

Now, I’ve tried several ways to create external constructors that mimic the Symmetric Triangular distribution code:

Attempt 1:

function JohnsonDist(member::T0, γ::T1, δ::T2, ξ::T3, λ::T4;check_args=true) where {T0<:AbstractString, T1<:Real, T2<:Real, T3<:Real, T4<:Real}
    check_args && @check_args(JohnsonDist, δ ≥ zero(δ))
    return JohnsonDist{T0,T1,T2,T3,T4}(member::T0, γ::T1, δ::T2, ξ::T3, λ::T4) 
end

Then I try to create a distribution from the struct:

julia> myj1=JohnsonDist("SB",0.5,1.0,1.0,1.0)
ERROR: MethodError: no method matching JohnsonDist{String, Float64, Float64, Float64, Float64}(::String, ::Float64, ::Float64, ::Float64, ::Float
64)
Stacktrace:
 [1] JohnsonDist(member::String, γ::Float64, δ::Float64, ξ::Float64, λ::Float64; check_args::Bool)
   @ Main .\Untitled-1:61
 [2] JohnsonDist(member::String, γ::Float64, δ::Float64, ξ::Float64, λ::Float64)
   @ Main .\Untitled-1:60
 [3] top-level scope
   @ REPL[6]:1 

Attempt 2: instead of @check_args, use if-then statements. This appears to result in an infinite loop.

function JohnsonDist(member::T0, γ::T1, δ::T2, ξ::T3, λ::T4;check_args=true) where {T0<:AbstractString, T1<:Real, T2<:Real, T3<:Real, T4<:Real}
    if check_args 

        if length(member) > 2 && lowercase(member) ≠ "auto"
            throw(ErrorException("Member name must be SL, SU, SB, SN, or AUTO. Your member you used was $(member)"))
        end

        if lowercase(member)=="sb" 
            if δ ≤ 0 || λ ≤ 0 
                throw(ErrorException("For Sb member, δ and λ must be greater than zero; δ = $(δ), λ = $(λ)"))
            end
        end

        return JohnsonDist(member,γ,δ,λ,ξ)

    end # if check_args 
end 

Then I try to create a distribution:

julia> myj2=JohnsonDist("SB",0.5,1.0,1.0,1.0)
ERROR: StackOverflowError:
Stacktrace:
     [1] _growend!
       @ .\array.jl:884 [inlined]
     [2] resize!
       @ .\array.jl:1104 [inlined]
     [3] map(f::typeof(lowercase), s::String)
       @ Base .\strings\basic.jl:614
     [4] lowercase
       @ .\strings\unicode.jl:545 [inlined]
     [5] JohnsonDist(member::String, γ::Float64, δ::Float64, ξ::Float64, λ::Float64; check_args::Bool)
       @ Main .\Untitled-1:48
     [6] JohnsonDist(member::String, γ::Float64, δ::Float64, ξ::Float64, λ::Float64)
       @ Main .\Untitled-1:42
     [7] JohnsonDist(member::String, γ::Float64, δ::Float64, ξ::Float64, λ::Float64; check_args::Bool)
       @ Main .\Untitled-1:54
--- the last 2 lines are repeated 13048 more times ---
 [26104] JohnsonDist(member::String, γ::Float64, δ::Float64, ξ::Float64, λ::Float64)
       @ Main .\Untitled-1:42

Third Attempt: a minimal external constructor, that uses promote()

JohnsonDist(member::String, γ::Real, δ::Real, λ::Real, ξ::Real) = JohnsonDist(promote(member)...,promote(γ,δ,λ,ξ)...)

Then I try to create a distribution – again appears to create an infinite loop.

ERROR: StackOverflowError:
Stacktrace:
 [1] JohnsonDist(member::String, γ::Float64, δ::Float64, λ::Float64, ξ::Float64) (repeats 65261 times)
   @ Main .\Untitled-1:65
 [2] top-level scope
   @ REPL[3]:1

So I’m lost now. The documentation is actually quite good, but the examples tend to all use arguments of the same type – whereas I have 1 argument of “AbstractString” and the rest are Reals (which could be ints, floats, or rationals).

I can kindof see why some of my code creates infinite loops, but then I’m puzzled why the SymTriangular code does not.

Anyhow, I’d appreciate some pointers on getting on the right track.

I think the issue is that the inner constructor is lacking the type annotation, which makes it identical (for dispatch) to the outer constructor. This works (slightly simplified):

module M1

struct JohnsonDist{T0<:AbstractString, T1<:Real}
    member::T0
    γ::T1

    JohnsonDist{T0,T1}(member::T0, γ::T1) where {T0<:AbstractString, T1<:Real} = new{T0, T1}(uppercase(member), γ)
end

function JohnsonDist(member :: T0, γ :: T1; check_args = true) where {T0 <: AbstractString, T1 <: Real}
    check_args  &&  println("Checking args");
    return JohnsonDist{T0, T1}(member, γ)
end

end # module

using .M1

@show M1.JohnsonDist("sb", 1.0)

Checking args
M1.JohnsonDist("sb", 1.0) = Main.M1.JohnsonDist{String, Float64}("SB", 1.0)
Main.M1.JohnsonDist{String, Float64}("SB", 1.0)