Confused with composite type constructors

Hello, I am new to Julia, I would like to understand language more deeply, so I have several question:
(I am using 1.12.x version)

First of all, where can I find some BNF like definition?

Another question, I tried to create this composite type (struct}

struct OMG2{X<: Number, Y<: String}
    a::Int128
    b::Int128

    function OMG2(x::T) where T<:Float64
        println("Float64")
        
      
    end

    function OMG2(x::T, y::U) where {T<: Int64,U <: Int64}
        println("Int64, Int64")

    end

    function OMG2(x::T, y::U) where {T<: Float32,U <: Float32}
        println("Float32, Float32")
   

    end

end

I don’t understand why if the constructors are within the struct the var types X, Y are not propagated to constructors:
Also seems that the constructors don’t have access to the var a and b

struct OMG2{X<: Number, Y<: String}
    a::Int128
    b::Int128

    function OMG2(x::Y)
        # Wont work <----
        println("THIS WONT WORK")
    end

    function OMG2(x::T) where T<:Float64
        println("Any")
      
      
    end

    function OMG2(x::T, y::U) where {T<: Int64,U <: Int64}
        println("Int64, Int64")


    end

    function OMG2(x::T, y::U) where {T<: Float32,U <: Float32}
        println("Float32, Float32")
   

    end

end

I would like to have some kind of restriction, in inner constructors, but seems that this restriction is applied only if I have something like that (constructors are defined out of the composite types, and I don’t understand why):

struct OMG3{X<: Number, Y<: String}
    a::Int128
    b::Int128


    function OMG3(x::T) where T<:Float64
        println("Float64")
        
    end
end


# Created constructor out of the struct, and I don't understand that the conditions {X<:Number, Y<: String, is applied just for this kind of constructor
julia> OMG3{Int64, String}() = println("Int64, String")

# Seems that it is is basically some kind of instance
julia> OMG3{Int64, String}.

flags       hash
instance    layout
name        parameters
super       types
julia> OMG3{Int64, String}.instance


# Constructor in the struct, seems that it is "empty"
julia> OMG3{Float64}.

body
var



# So, I have 2 constructors, but if I do:
julia> methods(OMG3)
# 1 method for type constructor:
 [1] OMG3(x::T) where T<:Float64
     @ ~/devel/julia/base64proj/base64test.jl:9

# I will see just the struct one, and I don't understand why.
# From one side, I have restrictions based on my <:, and that constructor is not included in the methods output (it is created outside of the struct), and from other side I have struct constructor, but I can change the type restriction and seems, that it doesn't have access to the struct attributes like a, b 

I am confused can somebody help me?

Please don’t take this the wrong way, but there seems to be a lot of confusion. Have you read the manual section about constructors (in your case particularly the Parametric section)?
I think that it can answer a lot of the easier doubts.

Having said that what are you trying to achieve? Are a and b initialized independently from the inputs?

Thanks to pointing me to the right direction.

I am not 100% sure (either) what you try to achieve. What are X and Y meant for? Usually parameters of a struct make a field type “visible”. Also your constructors print, but never construct anything.

Here is an example where these two points are fixed. I’ll also add an inner constructor

struct CT{X<:Number, Y}
    a::X
    b::Y
    function CT(x::T) where {T}
        return new{T,T}(x, x+1)
    end
end

The return is not necessary, but I always add it for clarity. Note that this inner constructor overwrites the default one (which is then no longer usable), which would be CT{Int,Int}(1,2) or even in short CT(1,2) – but that is now no longer there.
Internal ones use new to create the type: The one I write does allow

julia> CT(1)
CT{Int64, Int64}(1, 2)

and sure replacing {T} by {T<:Float64} would restrict the type. But only do that is it Is really necessary. Even only use that if you need the T for the unconstrained case. Here we do need it within the new.

For the rest, I am a bit unsure what your goals are, but maybe this helps a bit on the inner constructors and the parameters.

And welcome to the forum! :waving_hand:

1 Like

I suggest playing with ordinary types quite a bit, before going anywhere near inner constructors.

struct CT2{X<:Number, Y}  # like kellertuer's 
    a::X
    b::Y
end

struct OMG4{X<: Number, Y<: String}  # like yours
    a::Int128
    b::Int128
end

CT does this – the point of its type parameters is that they tell us what’s stored inside:

julia> methods(CT2)
# 1 method for type constructor:
 [1] CT2(a::X, b::Y) where {X<:Number, Y}
     @ REPL[77]:2

julia> CT2(1.0, "msg")  # it figures out X, Y
CT2{Float64, String}(1.0, "msg")

julia> CT2{ComplexF32, Symbol}(1.0, :msg)  # another method, converts a
CT2{ComplexF32, Symbol}(1.0f0 + 0.0f0im, :msg)

But OMG is weird, it uses type parameters to store something completely independent of the fields. You can do this but you need a reason? (Val{T}() is the built-in way to store things in type parameters, but it has no fields.)

julia> methods(OMG4)  # no friendly constructors, as no way to guess X, Y
# 0 methods for type constructor

julia> OMG4{ComplexF32, String}(10, 20) 
OMG4{ComplexF32, String}(10, 20)

julia> methods(OMG4{ComplexF32, String})
# 1 method for type constructor:
 [1] (var"#ctor-self#"::Type{OMG4{X, Y}} where {X<:Number, Y<:String})(a, b)
     @ REPL[78]:2
1 Like

In short, an inner constructor is just an ordinary function, with any arguments you like. The only way it differs from any other function is that it can call the special function new to instantiate an object of the struct. If the struct has parameters, all of them must be included in the new call, e.g. like new{T, Int}(2, 3). The return value of the new call should be returned from the constructor.

Actually, a constructor can choose to not call new, in which case it will not instantiate an object. It can return something else (e.g. nothing, if it only prints something). It’s up to you, but the ordinary use of constructors is to instantiate an object of the struct with a new call, and return it.

I am just trying to understand some language decisions:

Basically what I don’t understand why composite type constraints are not propagated to constructor

struct A{X<: Number, Y<: String}
    a::Int128
    b::Y

    function A(c::X)
        return new{X,Y}(c, c+"1")
    end
    
end

^ function A doesn’t have access to the X constraint, I don’t understand why.
But If I create something like

struct B{X<: Int64, Y<: String}
    a::Int128
    b::Y

    function B() where T
        println("Just println")
    end
    
end

B{Int64, String}() = println("Constraints are used here") # it uses constraint
# So I can call it
julia> B{Int64, String}()
Constraints are used here

# and I created some constructor as I understand, but If do methods(B), 
# it doesn't contains the B{Int64, String}, How can I list all candidates of the dispatch?

julia> methods(B)
# 1 method for type constructor:
 [1] B() where T
     @ ~/devel/julia/base64proj/base64test.jl:19

# Another thing related to constraints
# in this case, seems that constrains are used from the top definition
julia> B{Float64, String}() = println("Constraints are used here")
ERROR: TypeError: in B, in X, expected X<:Int64, got Type{Float64}
Stacktrace:
 [1] top-level scope
   @ REPL[31]:1


This is always an error, unless X is defined. It’s exactly like writing f(x::Float33) = "ok", since there is no such type, you get an error.

The expected syntax is f(x::T) where T = println("got object of type ", T) to make clear that T is a variable we are defining here, just like x.

I don’t know what you mean by constraints. You can add methods to types, for instance (not highly recommended), but this will fail if your definition contradicts the type’s:

julia> Base.Array{Int64, String}() = println("my weird method has been called")

julia> Array{Int64, String}()
my weird method has been called

julia> struct Mine{A<:Number} end

julia> Mine{Int}() = "int"

julia> Mine{String}() = "fails"
ERROR: TypeError: in Mine, in A, expected A<:Number, got Type{String}

Again, these are things you can think about, without mentioning inner constructors.

but the X is defined in

struct A{X<: Number, Y<: String}
    a::Int128
    b::Y

    function A(c::X)
...

The constraints will be enforced when the inner constructor calls new, not when the inner constructor is called.

This is an outer constructor. It’s still an ordinary function, except for the type parameters in {}, which aren’t allowed in ordinary functions. But, still, the semantics is that of an ordinary function, it checks the parameters you have specified, and finds the function, and calls it.

This one, I do not fully understand. I would have thought methods(B) would include your outer constructor, but it is only visible with methods(B{Int, String}). And, apparently it checks the parameter constraints from the struct when it can’t find anything else to call, even though there are no more functions to call. It looks a bit weird.

No, the inner constructor is just like any ordinary function, there is no X in its scope. The X parameter of the struct is instantiated when the inner constructor calls new, but it isn’t in the scope of the constructor.

1 Like

I know, that it is not

So, as I understand, without new it is just “binded” method to struct, and it is visible to method function. But, If I want to enforce the under hood things, I have to call new?

So, the outer constructor behaves differently, or somehow it calls new() function implicitly, but in the inner constructor has to be called explicitly?

It is new which instantiates the object. Without a call to new, no object is instantiated. If you want to enforce some value constraint, it should be done before calling new. Like:

struct NegativeInt{T<:Integer}
    i::T

    function NegativeInt(i::Integer)
        i < 0 || error("i must be negative")
        return new{typeof(i)}(i)
    end
end

The outer constructor can’t instantiate the object by itself, there is no new available to an outer constructor. It must call an inner constructor to do that. Continuing the example above:

function NegativeInt(x::Float64)
    i = floor(Int, x)
    return NegativeInt(i)   # this calls the inner constructor, since i is an Int
end

Typically, one uses one or more inner constructors to enforce mandatory constraints, since there is no way to create an object without calling an inner constructor.

Outer constructors are typically used for making various useful variations for creating an object. E.g. which require some non-trivial conversions or computations.

Alternatively, one can use entirely ordinary functions

function negativeint(m::AbstractMatrix{Float64})
    x = nextlargesteigenvaluewhichweknowtobeintegral(m)
    return NegativeInt(round(Int, x))
end
1 Like

Thank you for your time.