When to use promote on parametric constructors

I’m #newtojulia and working on converting a C++ project (hence my habit to type everything). I’m trying to be flexible and using parametric composite types (see below) and bumped into the common error of “no method matching…”. The docs outline that I can use promote() convert() to handle my situation, but I’m wondering if that is the “right way” OR am I artificially restricting myself by using a parametric constructor?

If this is a “it depends” answer, then when would I use promote convert and when would I not use a parametric constructor?


MWE (Edit to make it more accurate to my exact problem):

struct MyStruct{T<:AbstractFloat}

    function MyStruct{T}(x::Vector{T}, y::Vector{T}, z::T) where T<:AbstractFloat
        new(x, y, z)

xx = [1.0, 2.0, 3.0] #Vector{Float64}
yy = [4.0, 5.0, 6.0] #Vector{Float64}
zz = 1 # Integer
m1 = MyStruct(xx, yy, zz) # MethodError: no method matching MyStruct(::Vector{Float64}, ::Vector{Float64}, ::Int64)

which can be fixed by

MyStruct(x::Vector{T}, y::Vector{T}, z::Real) where T<:AbstractFloat = MyStruct{T}(x, y, T(z))

Must your elements be <:AbstractFloat, or could they be <:Real? In your example, promote will not convert the Ints to anything that <:AbstractFloat. You would need to convert instead.

julia> promote(1,2,3)
(1, 2, 3)

julia> typeof(ans)
Tuple{Int64, Int64, Int64}

julia> convert(AbstractFloat, 5)

Converting may make it more ergonomic, but it might mask errors, that part ‘it depends’. Since you’re coming from C++ where implicit conversion between numeric types is common, I would think you’re likely opt for converting. Julia allows mixed int and float expressions, but it is also strict about not promoting int arguments to match a float argument. That makes it hard to infer what expected behavior is, so that probably motivates your question.

1 Like

Indeed I actually did just implement a convenience constructor using convert xD Actually I realize now that my MWE is a little misconstrued and I think I’ll go back and edit it to reflect a better reality.

They could certainly be <:Real, but they’ll almost guaranteed be used in floating point math, hence my intuition to just type them that way. The Julia docs do say that typing everything isn’t the way to go to make things faster though.

You’re getting at the heart of my problem, which is trying to do things the Julian way, and if keeping things Real and letting the compiler figure it all out then that’s fine with me.

Here’s just me slapping away at the REPL for a minute:

julia> struct TestStruct{T}; x::T; y::T; z::T; end

julia> TestStruct(1,2.0,3//1) # mixed types do not work
ERROR: MethodError: no method matching TestStruct(::Int64, ::Float64, ::Rational{Int64})
Closest candidates are:
  TestStruct(::T, ::T, ::T) where T at REPL[487]:1
 [1] top-level scope
   @ REPL[488]:1

julia> methods(TestStruct) # see that the default constructor requires all arguments of matched type
# 1 method for type constructor:
[1] TestStruct(x::T, y::T, z::T) where T in Main at REPL[487]:1

julia> TestStruct(x,y,z) = TestStruct(promote(x,y,z)...) # define a promoting constructor

julia> TestStruct(1,2.0,3//1)
TestStruct{Float64}(1.0, 2.0, 3.0)

Note that I didn’t use an explicit inner constructor in this example. The default inner constructor was good enough for me. In your MWE it should be too, although perhaps in your example you need a little more. Inner constructors are mostly useful for creating invariants (for example enforcing that x>=0).

I also didn’t bother to constrain the parameter T, although constraining it to Real or Number would be very reasonable. AbstractFloat is something that should rarely be necessary (it’s mostly used for dispatch on basic numerical functions).

EDIT: my response doesn’t look especially useful to your revised MWE.


Another option is to just include the .0 to explicitly make the value a float, which I try to be in the habit of anyway with C++ since I’ve been bit by the compiler not implicitly converting when I thought it would (e.g. integer division). This might just be the most clear solution.

1 Like

But it was still helpful, in particular the bit about not using <:AbstractFloat. Thanks!