# Limitations of \`Base.convert\` for the construction of parametric types

**URL:** <https://discourse.julialang.org/t/limitations-of-base-convert-for-the-construction-of-parametric-types/127977>\
**Category:** General Usage\
**Tags:** parametric-types, convert, constructors\
**Created:** [April 11, 2025, 12:37pm UTC](https://discourse.julialang.org/t/limitations-of-base-convert-for-the-construction-of-parametric-types/127977 "2025-04-11T12:37:40Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![heliosdrm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/heliosdrm/32/3851_2.png) [@heliosdrm](https://discourse.julialang.org/u/heliosdrm)\
**Post date:** [April 11, 2025, 12:37pm UTC](https://discourse.julialang.org/t/limitations-of-base-convert-for-the-construction-of-parametric-types/127977/1 "2025-04-11T12:37:40Z")

</div>

I have a couple of user-defined types in which one contains the other, as in the following minimal example:

```julia
struct MyType
    x::Float64
end

struct Container
    c::MyType
end

```

In this example, `MyType` is some kind of `Float64` with additional custom properties (methods that apply to it but not to regular numbers), and it would make sense to create `Container`s directly from the underlying number. In other words, I would like to be able to create a `Container(MyType(x))` by just writing `Container(x)`.

Of course, this (and other conversions) are conveniently handled if I also define:

```julia
Base.convert(::Type{MyType}, x::Real) = MyType(x)

```

However, the solution is not as straightforward if the types are parametric. Namely, if I define:

```julia
struct ParType{T}
    x::T
end

struct ParContainer{T}
    c::ParType{T}
end

Base.convert(::Type{ParType{T}}, x::T) where T = ParType(x)

```

Then `ParContainer(1)` throws `ERROR: MethodError: no method matching ParContainer(::Int64)`.

It does work if I give the parameter of `ParContainer`, i.e. `ParContainer{Int}(1)`. Another workaround is to define a parameter-less constructor explicitly:

```julia
ParContainer(x::T) where T = ParContainer{T}(x)

```

However, I would prefer not having to define new constructors for `ParContainer`, and rely only on methods dispatching on `ParType`, like happens with `Base.convert`. Is there a way of doing this?

---

<div class="post-metadata">

**Author:** ![mikmoore](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mikmoore/32/31109_2.png) [@mikmoore](https://discourse.julialang.org/u/mikmoore)\
**Post date:** [April 11, 2025, 2:21pm UTC](https://discourse.julialang.org/t/limitations-of-base-convert-for-the-construction-of-parametric-types/127977/2 "2025-04-11T14:21:09Z")

</div>

Does `Base.convert(::Type{<:ParType}, x) = ParType(x)` do what you want? Or maybe `Base.convert(T::Type{<:ParType}, x) = T(x)`?

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [April 11, 2025, 5:41pm UTC](https://discourse.julialang.org/t/limitations-of-base-convert-for-the-construction-of-parametric-types/127977/3 "2025-04-11T17:41:54Z")

</div>

This issue doesn’t really involve `convert` or nested types, that part is actually fine here. Parametric type definitions just don’t automatically make the parameter-less constructor methods that many people want. You have to define them yourself, which you did.

```julia
julia> struct MyType
           x::Float64
       end

julia> methods(MyType) # one for known field, one for conversions
# 2 methods for type constructor:
 [1] MyType(x::Float64)
     @ REPL[13]:2
 [2] MyType(x)
     @ REPL[13]:2

julia> struct MyType2{T} # useless parameter
           x::Float64
       end

julia> methods(MyType2) # all gone!
# 0 methods for type constructor

julia> methods(MyType2.body) # parametric constructor still here
# 1 method for type constructor:
 [1] (var"#ctor-self#"::Type{MyType2{T}} where T)(x)
     @ REPL[16]:2

```

The equivalent approach to what you demonstrated would be:

```julia
MyType2(x::T) where T = MyType{T}(x)

```

You might spot the problem here: `T` is a useless parameter and `x` would be converted to `Float64` prior to successful instantiation, so there’s no reason for `T` to match `typeof(x)`. We don’t need `MyType2{Int64}(1.0)`, in fact we’d have more type stability if we just instantiated `MyType{Float64}(x)`. There isn’t a good automatic way to compute type parameters entirely from inputs. A more practical but complex example would be inputs with different types for fields that share only 1 parameter; it’s not clear if promotion is desired, and you’d still have to implement `promote` yourself. I think there is an issue about adding these default constructors anyway, letting us opt out manually like usual, that ended up saying it’d be breaking.

---

<div class="post-metadata">

**Author:** ![heliosdrm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/heliosdrm/32/3851_2.png) [@heliosdrm](https://discourse.julialang.org/u/heliosdrm)\
**Post date:** [April 11, 2025, 8:59pm UTC](https://discourse.julialang.org/t/limitations-of-base-convert-for-the-construction-of-parametric-types/127977/4 "2025-04-11T20:59:15Z")

</div>

@mikmoore : no, the result is the same with the alternatives that you suggest.

@Benny : you explanation clarifies some things indeed (and now I understand better how the constructors of parametric types work, thank you!). I would have expected my example to work as desired, since the parameter of my container type is not “useless”, so `ParContainer` does have a parameter-less constructor:

```julia
julia> struct ParContainer{T}
           c::ParType{T}
       end

julia> methods(ParContainer)
# 1 method for type constructor:
 [1] ParContainer(c::ParType{T}) where T
     @ REPL[5]:2

```

Now, I understand that the problem is that I made an excessively broad interpretation of [this statement about `convert`](https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#When-is-convert-called?):

> [@](#):
>
> The following language constructs call `convert`:  
> (…)
> 
> - Assigning to a field of an object converts to the declared type of the field.

I thought that “assigning to a field of an object” would also apply to the action of default constructors, but I see that this is not the case. The default constructor is like a regular function with respect to this, and `convert` is not called to match the types of function arguments. It only _seems_ to apply to nonparametric types because they also have default constructors with generic arguments. Right?

And I guess that this won’t change, if as you say:

> [@Benny](#):
>
> I think there is an issue about adding these default constructors anyway, letting us opt out manually like usual, that ended up saying it’d be breaking.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [April 11, 2025, 9:35pm UTC](https://discourse.julialang.org/t/limitations-of-base-convert-for-the-construction-of-parametric-types/127977/5 "2025-04-11T21:35:25Z")

</div>

> [@heliosdrm](#):
>
> I thought that “assigning to a field of an object” would also apply to the action of default constructors, but I see that this is not the case.

That occurs within the (possibly automatic) constructor method; it cannot occur during the prior method dispatch. In your case, the `ParContainer` call won’t do any automatic conversion, and the only available method it can dispatch to, `ParContainer(c::ParType{T})`, only works on a `ParType` input.

I suppose a reason against automatically making the `ParContainer(x::T) where T = ParContainer{T}(x)` method is that there’s no language-level guarantee that the `convert` would make a `ParType{S}` from `x::T` where `T==S`, so `ParContainer{T}` containing a `ParType{T}` would fail to hold `ParType{S}`. There isn’t even a guarantee that `convert` would make a `ParType` at all; you implement the method so you can do anything. I can still see a world where it is automatically made, but with all the nuances of instantiation, it really seems simpler to generally make our constructors ourselves while enjoying the more obvious freebies sometimes.
