Ref is not a concrete type - poorly documented?

I just learned from Why are ref's in structs type unstable? that Ref is not a concrete type, and so structs containing a Ref are not type-stable, e.g.

struct Foo
  a::Ref
end

Instead you have to use, for example, RefValue like

struct Bar
  a::RefValue
end

As a new-ish user, who was introduced to Ref as “if you have to use a mutable, global variable, make it a const Ref so that it is type stable” (I know this is out of date now, since we can now declare types for global variables), it was surprising that putting a Ref in a struct is type unstable.

The documentation for Core.Ref (C Interface · The Julia Language) does implicitly tell you this, as it has an example

julia> Ref(5)
Base.RefValue{Int64}(5)

where you can see that the concrete type is Base.RefValue rather than Ref, but there is no explicit discussion and no warning about type instability.

Question: what is best practice for using Ref, and should it be documented more clearly?

In a quick search, the Discourse topic linked above was the only discussion I found about Ref and type stability, and the only documentation for Ref seems to be the docstring - did I miss something? If not, I think more guidance for usage would be good.

For a bit more detail on why I care: I’ve fallen into a pattern where when I want a mostly immutable struct, but one or two fields need to be mutable, I’ve made those fields Ref variables, assuming that this was a safe and sensible thing to do. Is this bad practice? Should I just use a mutable struct instead? Is using Base.RefValue instead of Ref the right thing to do?
As Base.RefValue is not exported from Base, it doesn’t seem like an obvious thing to choose as a user!

I made an issue for this a bit ago too Ref docstring should note clearly that it's an abstract type · Issue #55321 · JuliaLang/julia · GitHub. I don’t really understand some of the responses made in that issue but hopefully this can be addressed at some point. Maybe commenting your support for the matter in that issue could be useful as well.

2 Likes

structs may have mutable struct fields.

@nsajko are you saying that I should replace

struct Bar
  a::RefValue{Int64}
end

with

mutable struct BazSubStruct
  b::Int64
end
struct Baz
  a::BazSubStruct
end

?

That seems like quite a bit of boiler-plate code, which I’d only bother with if it was performance critical…

The other option that is available as of Julia 1.10 (iirc might be 1.9) is to make a mutable struct with const fields.

2 Likes

I just really don’t get why we can’t just at least export RefValue. The reasoning, and preference for requiring users to define whole new structs is just bizarre and user-hostile to me.

6 Likes

Personally I don’t see that as an issue at all. Another alternative is to use a zero-dimensional array, e.g., Array{Int,0}.

NB: mutable state is often a bad choice anyway

Zero dimensional arrays are worse, slower, and heavier than RefValues.

1 Like

They’re supported, though.

If this is such an issue, why not implement a zero dimensional array type as a simple mutable wrapper:

mutable struct A{T} <: AbstractArray{T, 0}
    value::T
end
Base.getindex(a::A) = a.value
...

Then put it in a package and register it.

I don’t think someone should have to make such a package when a perfectly fine RefValue exists, other than it not being public for whatever reason. People already use Ref for this (and not necessarily knowing about RefValue due to poor documentation).

3 Likes

If you don’t know the reason for it not being public, why are you advocating making it public? Or maybe just make a PR making it public and see what the devs will say.

I’ve already made the thread linked in my first reply? By “for whatever reason” maybe replace it with a bizarre reason - see the linked thread.

3 Likes

Would there be anything wrong with this?

struct Foo{R<:Ref}
    a::R
end

Whenever I care about type stability, I just parameterize my structs and have the compiler figure out the types it needs.

4 Likes

Here’s the definition of RefValue, from
https://github.com/JuliaLang/julia/blob/master/base/refvalue.jl:

mutable struct RefValue{T} <: Ref{T}
    x::T
    RefValue{T}() where {T} = new()
    RefValue{T}(x) where {T} = new(x)
end
RefValue(x::T) where {T} = RefValue{T}(x)

…so I’d be surprised if the manual boilerplate version has any performance benefits

My guess is @jameson’s performance claim in the github issue was comparing RefValue wrapped in a struct to using a mutable struct directly. I’d be interested in better understanding why that is and the tradeoffs here. Specifically, let’s consider a wrapper to count function calls, as used in various packages for optimization, quadrature, et cetera. There are two obvious implementations:

struct Count{F}
    f::F
    count::Base.RefValue{Int}
    Count(f::F) where {F} = new{F}(f, Ref(0))
end
(c::Count)(args...) = (c.count[] += 1; c.f(args...))

or

mutable struct Count{F}
    const f::F
    count::Int
    Count(f::F) where {F} = new{F}(f, 0)
end
(c::Count)(args...) = (c.count += 1; c.f(args...))

Can anyone enlighten me as to the performance characteristics and tradeoffs of these alternatives?

Why isn’t Ref an AbstractRef and RefValue the go-to Ref anyway? It seems odd to be primarily instantiating the RefValue type with its supertype per the docstring, like habitually using AbstractFloat to make Float64 values. Ref has other subtypes and its constructor doesn’t always make a RefValue even among unary methods.

julia> subtypes(Ref)
6-element Vector{Any}:
 Base.CFunction
 Base.RefArray
 Base.RefValue
 Core.Compiler.RefValue
 Core.LLVMPtr
 Ptr
4 Likes

this :100: . I guess the practical answer is it’s too late to change anything.

IIUC, the current behavior is as if AbstractVector{Any}() would return Any[], doesn’t make much sense with respect to other parts of Julia