Conventions for mutable shared structure

I have a function which takes a user-defined callable object, say f, which when called with a vector of numbers is expected to return the value f(x) and the gradient \nabla f(x) at x. DiffBase.GradientResult is a natural choice for the result type, and it should be up to the user to decide whether to use forward or reverse AD, or something completely different.

The question: ForwardDiff.gradient! and similar operate by overwriting the same GradientResult structure. But I don’t want results of different calls to f to share any structure, because that would lead to hideous bugs. The way I see it, I have two interface choices:

  1. f(x) returns results that may share structure, the consumer copys it. Advantage: even if the user messes up, I am safe.
  2. f(x) is expected to return non-shared values, taking care of it itself.

Is there a reason one of these fits better into Julia than the other? I am leaning toward (1), since the overhead of copy is the least of my concerns ATM. Or are there other options?

I may not know much about art …

“the consumer copies it”

  • best not to rest the success of your good work rest on that being true

If you don’t want the returned values to share state, just return values that don’t share state (which seems to be option 2)?

Certainly, but f is supplied by the user, and it is tricky (or nearly impossible?) to test for a structure not being shared. Sharing structure can be a simple user error, and lead to bugs.

BTW, having benchmarked both solutions, the cost of copy seems to be below measurement error.

I have been thinking about a mechanism that copies data by default, but allows functions to signal that the data does not share structure. Something like

struct Unshared{T}
    value::T
end

ensure_unshared(x::Unshared) = x

@generated function ensure_unshared(x)
    :(Unshared($(isbits(x) ? :x : :(deepcopy(x)))))
end

which could be used as eg

struct MyType
    x
    MyType(x::Unshared) = new(x.value)
end

MyType(x) = MyType(ensure_unshared(x))

Does this make sense?