where a may or may not be modified by calculate, without affecting the x -> y mapping. For example, it could memoize results, collect statistics, etc.
Also, assume that calculate is not thread-safe, the intended usage is to make thread-local copies.
The question is whether there is a generic function one should use for that purpose, for which one would define a method for types that support calculate. One could define a method for Base.copy, but that’s not what it promises, it explicitly mentions a shallow copy. Base.deepcopy is too deep. Is there something in-between that is generally used?
What needs to be copied to what depth depends strongly on what you’re using it for, as well as the implementation of the data-type. There can be conventions that this is verb is called copyForThreading, but it’s really FooModule.copyForThreading or BarModule.copyForThreading. No need to make this different methods of the same function – generic code has no business trying to do something like that, it depends too much on implementation details.
Maybe there is value in having a single function with multiple methods? This would look like
"""For datastructures where "read-only"-style access is not threadsafe (e.g. due to
internal memoization), this creates an appropriate partial copy/view; otherwise, the
same datastructure is returned.
Note that type-piracy is especially bad in methods for this function.
"""
function Base.copyForThreading(data)
data
end
"""For datastructures where "read-only"-style accesses performed by `operation`
are not threadsafe (e.g. due to internal memoization), this creates an appropriate
copy/view; otherwise, the same datastructure is returned.
Defaults to `copyForThreading(data)`."""
function Base.copyForThreading(operation, data)
copyForThreading(data)
end
We had a long discussion about a name for this in Ferrite.jl#1070 where we finally settled for task_local_copy with the semantics roughly defined as “copy everything that needs to be independent for using this datastructure as indended in concurrent tasks”.
For now I settled on independent_copy. But choosing a name is the most difficult part
@foobar_lv2, I agree that context matters, still, I think that a generic interface for this would be nice since the use case is so common. Could fall back to identity for containers that are (recursively) immutable, and fail for the rest, explicitly leaving it up to the implementer of that type to provide a method.
That would be terrible API design. The entire point of having a common function that multi-dispatches to various methods is that you can write code like
thread_local_copy = copyForThreading(data)
without knowing the type of data.
The way you described the intention, this is supposed to be a no-op for most contexts, i.e. is supposed to be used for “weird” types where read-only looking operations are in fact not read-only.
So you must make identity the default behavior.
Or you must give up the common base function, and instead use FooModule.copyForThreading; then you can add methods for base types without type piracy.
To give an example: Suppose you want to write multi-threaded solver code that uses something something cholesky factors. You want that code to be generic over dense / sparse matrices.
If the cholesky factors are dense, then using them to solve something is afaiu read-only, and you don’t copy. If they are sparse, then afaiu using them is not read-only; there is a scratch-space attached, and it is thread-safe only in the sense that operations take a lock, but that means that you’re down to single-threading.
So a clever imlementation could copy the relevant data for sparse matrices; or it could even get away with making a very shallow copy, and using trylock to lazily make that copy deeper if you get any contention.