Strange type constructor quirk

*Edit: changed title to be more accurate
Hi, everyone. So, I`m not exactly new to julia but this feels like it belongs here as it is quite basic and still puzzles me.

Let’s consider the MWE in following code snippet:

struct B 
    x::Ref{Union{Nothing,Int}}
    y::Int
end

struct B2{T}
    x::Ref{Union{Nothing,Int}}
    y::T
end

So far, so good. Both structs only differ in the fact that B2 is a parametric type for the second variable, y. Here comes the bit that confused me:


b = B(nothing,1)# works, returns B(Base.RefValue{Union{Nothing, Int64}}(nothing), 1)

b2 = B2(nothing,1)# ERROR: MethodError: no method matching B2(::Nothing, ::Int64)
b2 = B2(convert(Ref{Union{Nothing,Int}},nothing),1) #works, returns B2{Int64}(Base.RefValue{Union{Nothing, Int64}}(nothing), 1)
b2 = B2{Int}(nothing, 1) #works, returns B2{Int64}(Base.RefValue{Union{Nothing, Int64}}(nothing), 1)

In the first line, julia seems to realize that I want b to hold a Ref to nothing. This seems to be because nothing can apparently be converted to the type Ref{Union{Nothing,Int}} (somewhat surprising).

However, if I consider the parametric type B2, this automatic conversion no longer works (line 2). This surprises me even more, because the type T has nothing to do with x.

This can be cirumvented by either converting nothing explicitly, or providing the type T directly.

  1. But why does julia not do again what it did in the first example and convert this automagically?
  2. Anyway, is this rather unintuitive type conversion something that one can rely on, or could it be that it is unintended behaviour and might change in future minor releases?

Thanks for your insight!

It’s not a matter of type inference, it’s simply because a B(x, y) = new(x, y) method (new tries to convert x and y to the annotated field types) was automatically generated while B2(x, y) method wasn’t, which you basically showed. This makes it more apparent:

julia> methods(B)
# 2 methods for type constructor:
 [1] B(x::Ref{Union{Nothing, Int64}}, y::Int64)
     @ REPL[23]:3
 [2] B(x, y)
     @ REPL[23]:3

julia> methods(B2)
# 1 method for type constructor:
 [1] B2(x::Ref{Union{Nothing, Int64}}, y::T) where T
     @ REPL[23]:8

julia> methods(B2.body) # trick to find parametric constructors
# 2 methods for type constructor:
 [1] B2(x::Ref{Union{Nothing, Int64}}, y::T) where T
     @ REPL[23]:8
 [2] (var"#ctor-self#"::Type{B2{T}} where T)(x, y)
     @ REPL[23]:8
2 Likes

This is not an issue with type inference or conversion, it’s about what default constructors gets defined when you define a new type.

The issue is described in more detail in Autogenerate `(::Type{Foo})(x::T, y) where T` constructor for type Foo{T} · Issue #35053 · JuliaLang/julia · GitHub

5 Likes

Ah, that makes sense thanks for the clarification!
My title is quite inaccurate indeed.

I suppose it should be safe to rely on the automatic method definition then, looks like if anything is going to change it would be that these methods are also added for functions with parametric types.

This is just a note on a problem I had once when using Refs in a struct and relevant only if you are actually using something like that in your code.

I’m not on my computer so I can’t check if that’s still the case, but at least it used to be that Ref is actually an abstract type. You’d want to use Base.RefValue{Union{Nothing, Int}} as the type of the x field instead.

3 Likes

Thanks for the info. Youre right, I do want the functionality of RefValue. I have checked and Ref{T} is indeed an abstract type.

While it’s true that Ref is abstract, RefValue is internal to Base, an implementation detail not available to users.

Well, it definitively is available to users (meaning we can use it). it is just not guaranteed to keep it that way between any minor version bumps of Julia.

For this particular case, the only drawback is the extra maintenance burden on the developer, who would have to check compatibility with any minor version change of Julia.

But there is a silly workaround that uses only public API and would be preferred:

const RefNohtingInt = typeof(Ref{Union{Nothing, Int}}())

struct B 
    x::RefNothingInt
    y::Int
end

struct B2{T}
    x::RefNothingInt
    y::T
end

But I really don’t like this opaqueness of Refs. It is not obvious anywhere that is an abstract type. From the documentation, one would assume it is a concrete type

3 Likes