I need copy-on-write behaviour to protect fields of the large values I’m handing out to my users from cluttering their memory with useless copies.
Is there such thing as a julia Cow{T} akin to rust’s Cow<T> ?
Should I write it myself with some kind of, I don’t know:
mutable struct Cow{T}
_value::T
_owned::Bool
end
then very strict access control to such values ?
(N.B: the Cow values themselves would not be exposed to my users so it’d be okay that such a homebrew cow be not fullproof, and just helped me not mutate T without deepcopying it first and without cluttering my sources.)
You could use a simple ref-counted container, something like
mutable struct AtomicInt
@atomic v::Int
end
mutable struct CowMem{T}
contents :: Memory{T}
refcount :: AtomicInt
end
CowMem(::Type{T}, size) where T = CowMem(Memory{T}(undef, size), AtomicInt(1))
function share(cow:: CowMem)
@atomic cow.refcount.v += 1
CowMem(cow.contents, cow.refcount)
end
function free(cow:: CowMem)
@atomic cow.refcount.v += -1
end
function Base.getindex(cow:: CowMem, idx)
cow.contents[idx]
end
function Base.setindex!(cow:: CowMem, v, idx)
rc = cow.refcount
if rc.v != 1
cow.contents = copy(cow.contents)
@atomic rc.v += -1
cow.refcount = AtomicInt(1)
end
ctx=cow.contents
ctx[idx] = v
end
Then you can take a “cow-snapshot” with the share function, and release snapshots via the free function.
Note that different threads need different cow-snapshots (which are initially backed by the same memory), otherwise this is racy!
Also note that the atomic access in setindex! is kinda expensive / hard to optimize. I think there is a good argument that if (@atomic :unordered rc.v) != 0 is good enough, but it’s very hard to reason about atomic :unordered.
It’s worth bearing in mind that the mutability semantics of Julia and Rust come from different language families. Rust offers immutable or limited mutable references to objects, Julia objects themselves have immutable or mutable types. The ability to mutate _value::T means T is a mutable type (or if we speak more flexibly about mutation API, it could be a wrapper of one), and we access mutable instances solely by what are effectively unlimited mutable references. We just don’t have the kind of control to pull off to_mut for a single type.
Julia also doesn’t have Rust’s automatic Deref coercion, which seemlessly unwraps Cow<T> for methods taking immutable references to T. If you use a wrapper type in Julia, you either have to unwrap it manually first or you reimplement a bunch of methods to do it before repeating the call, and there’s no protection from mutation of a mutable T.
The closest thing to protection from mutation is sharing an instance of an immutable type. The user can be forced to use a method to deep-copy to a mutable type for mutation, call it to_mut if you like. To share instead of copy the data of an immutable instance (like if you assign it to an inlined element or field), you’d need to wrap it in something like the mutable Ref, though you’d also need to do your best to obstruct the user from reassigning it. On top of that, you’d need to reimplement non-mutating methods for both the Ref-ed immutable version and mutable version.